Forked from
Stud.IP / Stud.IP
1423 commits behind, 371 commits ahead of the upstream repository.
-
Jan-Hendrik Willms authored
Closes #4374 Merge request studip/studip!3179
Jan-Hendrik Willms authoredCloses #4374 Merge request studip/studip!3179
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
StudipAuthAbstract.class.php 19.31 KiB
<?php
// +---------------------------------------------------------------------------+
// This file is part of Stud.IP
// StudipAuthAbstract.class.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!'),
__METHOD__
);
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_msg = sprintf(
_('Methode %s nicht implementiert!'),
__METHOD__
);
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]);
}
}