Skip to content
Snippets Groups Projects
Commit 5aae0637 authored by André Noack's avatar André Noack Committed by Jan-Hendrik Willms
Browse files

Resolve "OpenID Connect als SSO AuthPlugin"

parent 84e79538
No related branches found
No related tags found
No related merge requests found
Showing with 578 additions and 419 deletions
...@@ -47,6 +47,7 @@ ...@@ -47,6 +47,7 @@
"slim/slim": "4.7.1", "slim/slim": "4.7.1",
"php-di/php-di": "6.3.4", "php-di/php-di": "6.3.4",
"symfony/console": "5.3.6", "symfony/console": "5.3.6",
"symfony/process": "^5.4" "symfony/process": "^5.4",
"jumbojett/openid-connect-php": "^0.9.2"
} }
} }
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "59006c71d43d6f32f0445636c803208d", "content-hash": "2fa6a856bbe442274874aeabe26f054b",
"packages": [ "packages": [
{ {
"name": "algo26-matthias/idna-convert", "name": "algo26-matthias/idna-convert",
...@@ -545,6 +545,48 @@ ...@@ -545,6 +545,48 @@
}, },
"time": "2019-08-18T20:01:55+00:00" "time": "2019-08-18T20:01:55+00:00"
}, },
{
"name": "jumbojett/openid-connect-php",
"version": "v0.9.5",
"source": {
"type": "git",
"url": "https://github.com/jumbojett/OpenID-Connect-PHP.git",
"reference": "14991f706675b13dd1f72291bfd779f144454d64"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jumbojett/OpenID-Connect-PHP/zipball/14991f706675b13dd1f72291bfd779f144454d64",
"reference": "14991f706675b13dd1f72291bfd779f144454d64",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"paragonie/random_compat": ">=2",
"php": ">=5.4",
"phpseclib/phpseclib": "~2.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8",
"roave/security-advisories": "dev-master"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"description": "Bare-bones OpenID Connect client",
"support": {
"issues": "https://github.com/jumbojett/OpenID-Connect-PHP/issues",
"source": "https://github.com/jumbojett/OpenID-Connect-PHP/tree/v0.9.5"
},
"time": "2021-11-24T16:11:49+00:00"
},
{ {
"name": "kub-at/php-simple-html-dom-parser", "name": "kub-at/php-simple-html-dom-parser",
"version": "1.9.1", "version": "1.9.1",
......
...@@ -326,6 +326,26 @@ $STUDIP_AUTH_CONFIG_CAS = array("host" => "cas.studip.de", ...@@ -326,6 +326,26 @@ $STUDIP_AUTH_CONFIG_CAS = array("host" => "cas.studip.de",
"auth_user_md5.Email" => array("callback" => "getUserData", "map_args" => "email"), "auth_user_md5.Email" => array("callback" => "getUserData", "map_args" => "email"),
"auth_user_md5.perms" => array("callback" => "getUserData", "map_args" => "status"))); "auth_user_md5.perms" => array("callback" => "getUserData", "map_args" => "status")));
//example of OpenID Connect
$STUDIP_AUTH_CONFIG_GOOGLE = [
'provider_url' => 'https://accounts.google.com',
'client_id' => '',
'client_secret' => '',
'plugin_class' => 'StudipAuthOIDC',
'plugin_name' => 'google',
'domain' => 'google',
'plugin_fullname' => 'Google',
'login_description' => 'Login with Google',
'ssl_options' => ['certPath' => null, 'verifyPeer' => true, 'verifyHost' => true],
'user_data_mapping' =>
['auth_user_md5.username' => ['callback' => 'dummy', 'map_args' => ''],
'auth_user_md5.password' => ['callback' => 'dummy', 'map_args' => ''],
'auth_user_md5.Email' => ['callback' => 'getUserData', 'map_args' => 'email'],
'auth_user_md5.Nachname' => ['callback' => 'getUserData', 'map_args' => 'family_name'],
'auth_user_md5.Vorname' => ['callback' => 'getUserData', 'map_args' => 'given_name']
]
];
$STUDIP_AUTH_CONFIG_LTI = [ $STUDIP_AUTH_CONFIG_LTI = [
'consumer_keys' => [ 'consumer_keys' => [
// 'domain' is optional, default is value of consumer_key // 'domain' is optional, default is value of consumer_key
......
<?php <?php
# Lifter003: TEST
# Lifter007: TODO
# Lifter010: DONE - no html
// +---------------------------------------------------------------------------+ // +---------------------------------------------------------------------------+
// This file is part of Stud.IP // This file is part of Stud.IP
// StudipAuthAbstract.class.php // StudipAuthAbstract.class.php
...@@ -36,29 +32,27 @@ ...@@ -36,29 +32,27 @@
* won't do that for you ! * won't do that for you !
* *
* @abstract * @abstract
* @access public
* @author André Noack <noack@data-quest.de> * @author André Noack <noack@data-quest.de>
* @package * @package
*/ */
class StudipAuthAbstract { class StudipAuthAbstract
{
/** /**
* contains error message, if authentication fails * contains error message, if authentication fails
* *
* *
* @access public * @var string $error_msg
* @var string
*/ */
var $error_msg; public $error_msg;
/** /**
* indicates whether the authenticated user logs in for the first time * indicates whether the authenticated user logs in for the first time
* *
* *
* @access public * @var bool $is_new_user
* @var bool
*/ */
var $is_new_user = false; public $is_new_user = false;
/** /**
* array of user domains to assign to each user, can be set in local.inc * array of user domains to assign to each user, can be set in local.inc
...@@ -66,7 +60,7 @@ class StudipAuthAbstract { ...@@ -66,7 +60,7 @@ class StudipAuthAbstract {
* @access public * @access public
* @var array $user_domains * @var array $user_domains
*/ */
var $user_domains; public $user_domains;
/** /**
* associative array with mapping for database fields * associative array with mapping for database fields
...@@ -74,53 +68,54 @@ class StudipAuthAbstract { ...@@ -74,53 +68,54 @@ class StudipAuthAbstract {
* associative array with mapping for database fields, * associative array with mapping for database fields,
* should be set in local.inc * should be set in local.inc
* structure : * structure :
* array("<table name>.<field name>" => array( "callback" => "<name of callback method used for data retrieval>", * array('<table name>.<field name>' => array( 'callback' => '<name of callback method used for data retrieval>',
* "map_args" => "<arguments passed to callback method>")) * 'map_args' => '<arguments passed to callback method>'))
* @access public
* @var array $user_data_mapping * @var array $user_data_mapping
*/ */
var $user_data_mapping = null; public $user_data_mapping = null;
/** /**
* name of the plugin * name of the plugin
* *
* name of the plugin (last part of class name) is set in the constructor * name of the plugin (last part of class name) is set in the constructor
* @access public * @var string $plugin_name
* @var string
*/ */
var $plugin_name; public $plugin_name;
/** /**
* text, which precedes error message for the plugin * text, which precedes error message for the plugin
* *
* *
* @access public * @var string $error_head
* @var string
*/ */
var $error_head; public $error_head;
/**
* @var $plugin_instances
*/
private static $plugin_instances; private static $plugin_instances;
/** /**
* static method to instantiate and retrieve a reference to an object (singleton) * static method to instantiate and retrieve a reference to an object (singleton)
* *
* use always this method to instantiate a plugin object, it will ensure that only one object of each * always use this method to instantiate a plugin object, it will ensure that only one object of each
* plugin will exist * plugin will exist
* @access public * @param string $plugin_name name of plugin, if omitted an array with all plugin objects will be returned
* @static
* @param string 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 * @return mixed either a reference to the plugin with the passed name, or an array with references to all plugins
*/ */
static function getInstance($plugin_name = false) public static function getInstance($plugin_name = false)
{ {
if (!is_array(self::$plugin_instances)) { if (!is_array(self::$plugin_instances)) {
foreach ($GLOBALS['STUDIP_AUTH_PLUGIN'] as $plugin) { foreach ($GLOBALS['STUDIP_AUTH_PLUGIN'] as $plugin) {
$plugin = "StudipAuth" . $plugin; $config = $GLOBALS['STUDIP_AUTH_CONFIG_' . strtoupper($plugin)];
include_once "lib/classes/auth_plugins/" . $plugin . ".class.php"; $plugin_class = $config['plugin_class'] ?? 'StudipAuth' . $plugin;
self::$plugin_instances[mb_strtoupper($plugin)] = new $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[mb_strtoupper("StudipAuth" . $plugin_name)] : self::$plugin_instances; return ($plugin_name) ? self::$plugin_instances[strtoupper($plugin_name)] : self::$plugin_instances;
} }
/** /**
...@@ -129,13 +124,11 @@ class StudipAuthAbstract { ...@@ -129,13 +124,11 @@ class StudipAuthAbstract {
* if authentication fails in one plugin, the error message is stored and the next plugin is used * 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 * if authentication succeeds, the uid element in the returned array will contain the Stud.IP user id
* *
* @access public * @param string $username the username to check
* @static * @param string $password the password to check
* @param string the username to check
* @param string the password to check
* @return array structure: array('uid'=>'string <Stud.IP user id>','error'=>'string <error message>','is_new_user'=>'bool') * @return array structure: array('uid'=>'string <Stud.IP user id>','error'=>'string <error message>','is_new_user'=>'bool')
*/ */
static function CheckAuthentication($username, $password) public static function CheckAuthentication($username, $password)
{ {
$plugins = StudipAuthAbstract::GetInstance(); $plugins = StudipAuthAbstract::GetInstance();
...@@ -157,21 +150,21 @@ class StudipAuthAbstract { ...@@ -157,21 +150,21 @@ class StudipAuthAbstract {
$exp_d = UserConfig::get($user['user_id'])->EXPIRATION_DATE; $exp_d = UserConfig::get($user['user_id'])->EXPIRATION_DATE;
if ($exp_d > 0 && $exp_d < time()) { if ($exp_d > 0 && $exp_d < time()) {
$error .= _("Dieses Benutzerkonto ist abgelaufen.<br> Wenden Sie sich bitte an die Administration.") . "<BR>"; $error .= _('Dieses Benutzerkonto ist abgelaufen.<br> Wenden Sie sich bitte an die Administration.') . '<BR>';
return ['uid' => false, 'error' => $error]; return ['uid' => false, 'error' => $error];
} else if ($locked == "1") { } else if ($locked) {
$error .= _("Dieser Benutzer ist gesperrt! Wenden Sie sich bitte an die Administration.") . "<BR>"; $error .= _('Dieser Benutzer ist gesperrt! Wenden Sie sich bitte an die Administration.') . '<BR>';
return ['uid' => false, 'error' => $error]; return ['uid' => false, 'error' => $error];
} else if ($key != '') { } else if ($key != '') {
return ['uid' => $uid, 'user' => $user, 'error' => $error, 'need_email_activation' => $uid]; return ['uid' => $uid, 'user' => $user, 'error' => $error, 'need_email_activation' => $uid];
} else if ($checkIPRange && !self::CheckIPRange()) { } else if ($checkIPRange && !self::CheckIPRange()) {
$error .= _("Der Login in Ihren Account ist aus diesem Netzwerk nicht erlaubt.") . "<BR>"; $error .= _('Der Login in Ihren Account ist aus diesem Netzwerk nicht erlaubt.') . '<BR>';
return ['uid' => false, 'error' => $error]; return ['uid' => false, 'error' => $error];
} }
} }
return ['uid' => $uid, 'user' => $user, 'error' => $error, 'is_new_user' => $object->is_new_user]; return ['uid' => $uid, 'user' => $user, 'error' => $error, 'is_new_user' => $object->is_new_user];
} else { } else {
$error .= (($object->error_head) ? ("<b>" . $object->error_head . ":</b> ") : "") . $object->error_msg . "<br>"; $error .= (($object->error_head) ? ('<b>' . $object->error_head . ':</b> ') : '') . $object->error_msg . '<br>';
} }
} }
return ['uid' => $uid, 'error' => $error]; return ['uid' => $uid, 'error' => $error];
...@@ -182,12 +175,10 @@ class StudipAuthAbstract { ...@@ -182,12 +175,10 @@ class StudipAuthAbstract {
* *
* all plugins are checked, the error messages are stored and returned * all plugins are checked, the error messages are stored and returned
* *
* @access public * @param string $username the username
* @static
* @param string the username
* @return array * @return array
*/ */
static function CheckUsername($username) public static function CheckUsername($username)
{ {
$plugins = StudipAuthAbstract::GetInstance(); $plugins = StudipAuthAbstract::GetInstance();
$error = false; $error = false;
...@@ -196,24 +187,23 @@ class StudipAuthAbstract { ...@@ -196,24 +187,23 @@ class StudipAuthAbstract {
if ($found = $object->isUsedUsername($username)) { if ($found = $object->isUsedUsername($username)) {
return ['found' => $found, 'error' => $error]; return ['found' => $found, 'error' => $error];
} else { } else {
$error .= (($object->error_head) ? ("<b>" . $object->error_head . ":</b> ") : "") . $object->error_msg . "<br>"; $error .= (($object->error_head) ? ('<b>' . $object->error_head . ':</b> ') : '') . $object->error_msg . '<br>';
} }
} }
return ['found' => $found, 'error' => $error]; return ['found' => $found, 'error' => $error];
} }
/** /**
* static method to check for a mapped field * static method to check for a mapped field
* *
* this method checks in the plugin with the passed name, if the passed * this method checks in the plugin with the passed name, if the passed
* Stud.IP DB field is mapped to an external data source * Stud.IP DB field is mapped to an external data source
* *
* @access public
* @static
* @param string the name of the db field must be in form '<table name>.<field name>' * @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 * @param string the name of the plugin to check
* @return bool true if the field is mapped, else false * @return bool true if the field is mapped, else false
*/ */
static function CheckField($field_name,$plugin_name) public static function CheckField($field_name, $plugin_name)
{ {
if (!$plugin_name) { if (!$plugin_name) {
return false; return false;
...@@ -230,7 +220,7 @@ class StudipAuthAbstract { ...@@ -230,7 +220,7 @@ class StudipAuthAbstract {
public static function CheckIPRange() public static function CheckIPRange()
{ {
$ip = $_SERVER['REMOTE_ADDR']; $ip = $_SERVER['REMOTE_ADDR'];
$version = mb_substr_count($ip, ':') > 1 ? 'V6' : 'V4'; // valid ip v6 addresses have atleast two colons $version = substr_count($ip, ':') > 1 ? 'V6' : 'V4'; // valid ip v6 addresses have atleast two colons
$method = 'CheckIPRange' . $version; $method = 'CheckIPRange' . $version;
if (is_array($GLOBALS['LOGIN_IP_RANGES'][$version])) { if (is_array($GLOBALS['LOGIN_IP_RANGES'][$version])) {
foreach ($GLOBALS['LOGIN_IP_RANGES'][$version] as $range) { foreach ($GLOBALS['LOGIN_IP_RANGES'][$version] as $range) {
...@@ -275,33 +265,32 @@ class StudipAuthAbstract { ...@@ -275,33 +265,32 @@ class StudipAuthAbstract {
$start = inet_pton($range['start']); $start = inet_pton($range['start']);
$end = inet_pton($range['end']); $end = inet_pton($range['end']);
return mb_strlen($ipv6) === mb_strlen($start) return strlen($ipv6) === strlen($start)
&& $ipv6 >= $start && $ipv6 <= $end; && $ipv6 >= $start && $ipv6 <= $end;
} }
/** /**
* Constructor * Constructor
* *
* the constructor is private, you should use StudipAuthAbstract::GetInstance($plugin_name) * 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 * 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 * 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 * each key of the $STUDIP_AUTH_CONFIG_<plugin name> array will become a member of the object
* *
* @access private * @param array $config
*
*/ */
function __construct() public function __construct($config = [])
{ {
$this->plugin_name = mb_strtolower(mb_substr(get_class($this),10));
//get configuration array set in local inc //get configuration array set in local inc
$config_var = $GLOBALS["STUDIP_AUTH_CONFIG_" . mb_strtoupper($this->plugin_name)]; 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 //assign each key in the config array as a member of the plugin object
if (isset($config_var)) { foreach ($config as $key => $value) {
foreach ($config_var as $key => $value) {
$this->$key = $value; $this->$key = $value;
} }
} }
}
/** /**
* authentication method * authentication method
...@@ -310,12 +299,11 @@ class StudipAuthAbstract { ...@@ -310,12 +299,11 @@ class StudipAuthAbstract {
* if authentication succeeds it calls StudipAuthAbstract::doDataMapping() to map data fields * 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 * if the authenticated user logs in for the first time it calls StudipAuthAbstract::doNewUserInit() to
* initialize the new user * initialize the new user
* @access private * @param string $username the username to check
* @param string the username to check * @param string $password the password to check
* @param string the password to check
* @return string if authentication succeeds the Stud.IP user , else false * @return string if authentication succeeds the Stud.IP user , else false
*/ */
function authenticateUser($username, $password) public function authenticateUser($username, $password)
{ {
$username = $this->verifyUsername($username); $username = $this->verifyUsername($username);
if ($this->isAuthenticated($username, $password)) { if ($this->isAuthenticated($username, $password)) {
...@@ -346,11 +334,11 @@ class StudipAuthAbstract { ...@@ -346,11 +334,11 @@ class StudipAuthAbstract {
if ($user) { if ($user) {
$auth_plugin = $user->auth_plugin; $auth_plugin = $user->auth_plugin;
if ($auth_plugin === null) { if ($auth_plugin === null) {
$this->error_msg = _("Dies ist ein vorläufiger Benutzer.") . "<br>"; $this->error_msg = _('Dies ist ein vorläufiger Benutzer.') . '<br>';
return false; return false;
} }
if ($auth_plugin != $this->plugin_name) { if ($auth_plugin != $this->plugin_name) {
$this->error_msg = sprintf(_("Dieser Benutzername wird bereits über %s authentifiziert!"),$auth_plugin) . "<br>"; $this->error_msg = sprintf(_('Dieser Benutzername wird bereits über %s authentifiziert!'), $auth_plugin) . '<br>';
return false; return false;
} }
return $user; return $user;
...@@ -389,7 +377,8 @@ class StudipAuthAbstract { ...@@ -389,7 +377,8 @@ class StudipAuthAbstract {
* @access private * @access private
* @param User the user object * @param User the user object
*/ */
function setUserDomains ($user) { function setUserDomains($user)
{
$user_domains = $this->getUserDomains(); $user_domains = $this->getUserDomains();
$uid = $user->id; $uid = $user->id;
if (isset($user_domains)) { if (isset($user_domains)) {
...@@ -439,17 +428,23 @@ class StudipAuthAbstract { ...@@ -439,17 +428,23 @@ class StudipAuthAbstract {
{ {
if ($user && is_array($this->user_data_mapping)) { if ($user && is_array($this->user_data_mapping)) {
foreach ($this->user_data_mapping as $key => $value) { foreach ($this->user_data_mapping as $key => $value) {
$callback = null;
if (method_exists($this, $value['callback'])) { if (method_exists($this, $value['callback'])) {
$split = explode(".",$key); $callback = [$this, $value['callback']];
} else if (is_callable($value['callback'])) {
$callback = $value['callback'];
}
if ($callback) {
$split = explode('.', $key);
$table = $split[0]; $table = $split[0];
$field = $split[1]; $field = $split[1];
if ($table == 'auth_user_md5' || $table == 'user_info') { if ($table === 'auth_user_md5' || $table === 'user_info') {
$mapped_value = call_user_func([$this, $value['callback']],$value['map_args']); $mapped_value = call_user_func($callback, $value['map_args']);
if (isset($mapped_value)) { if (isset($mapped_value)) {
$user->setValue($field, $mapped_value); $user->setValue($field, $mapped_value);
} }
} else { } else {
call_user_func([$this, $value['callback']],[$table,$field,$user,$value['map_args']]); call_user_func($callback, [$table, $field, $user, $value['map_args']]);
} }
} }
} }
...@@ -500,7 +495,7 @@ class StudipAuthAbstract { ...@@ -500,7 +495,7 @@ class StudipAuthAbstract {
*/ */
function isUsedUsername($username) function isUsedUsername($username)
{ {
$this->error_msg = sprintf(_("Methode %s nicht implementiert!"),get_class($this) . "::isUsedUsername()"); $this->error_msg = sprintf(_('Methode %s nicht implementiert!'), get_class($this) . '::isUsedUsername()');
return false; return false;
} }
...@@ -514,8 +509,9 @@ class StudipAuthAbstract { ...@@ -514,8 +509,9 @@ class StudipAuthAbstract {
* @param string the password * @param string the password
* @return bool true if authentication succeeds * @return bool true if authentication succeeds
*/ */
function isAuthenticated($username, $password) { function isAuthenticated($username, $password)
$this->error = sprintf(_("Methode %s nicht implementiert!"),get_class($this) . "::isAuthenticated()"); {
$this->error = sprintf(_('Methode %s nicht implementiert!'), get_class($this) . '::isAuthenticated()');
return false; return false;
} }
} }
...@@ -13,27 +13,33 @@ ...@@ -13,27 +13,33 @@
require_once 'composer/jasig/phpcas/CAS.php'; require_once 'composer/jasig/phpcas/CAS.php';
require_once 'lib/classes/cas/CAS_PGTStorage_Cache.php'; require_once 'lib/classes/cas/CAS_PGTStorage_Cache.php';
class StudipAuthCAS extends StudipAuthSSO { class StudipAuthCAS extends StudipAuthSSO
{
var $host; public $host;
var $port; public $port;
var $uri; public $uri;
var $cacert; public $cacert;
var $cas; public $cas;
var $userdata; public $userdata;
/** /**
* Constructor * Constructor
* *
* *
* @access public
* *
*/ */
function __construct() { public function __construct($config = [])
parent::__construct(); {
parent::__construct($config);
if (Request::option('sso')) { if (!isset($this->plugin_fullname)) {
$this->plugin_fullname = _('CAS');
}
if (!isset($this->login_description)) {
$this->login_description = _('für Single Sign On mit CAS');
}
if (Request::get('sso') === $this->plugin_name) {
$this->cas = new CAS_Client(CAS_VERSION_2_0, $this->proxy, $this->host, $this->port, $this->uri, false); $this->cas = new CAS_Client(CAS_VERSION_2_0, $this->proxy, $this->host, $this->port, $this->uri, false);
if ($this->proxy) { if ($this->proxy) {
...@@ -68,22 +74,22 @@ class StudipAuthCAS extends StudipAuthSSO { ...@@ -68,22 +74,22 @@ class StudipAuthCAS extends StudipAuthSSO {
return $this->getUser(); return $this->getUser();
} }
function getUserData($key){ function getUserData($key)
$userdataclassname = $GLOBALS["STUDIP_AUTH_CONFIG_CAS"]["user_data_mapping_class"]; {
if (empty($userdataclassname)){ $userdataclassname = $this->user_data_mapping_class;
echo ("ERROR: no userdataclassname specified."); if (!class_exists($userdataclassname)) {
Log::ERROR($this->plugin_name . ': no userdataclassname specified or found.');
return; return;
} }
require_once($userdataclassname . ".class.php");
// get the userdata // get the userdata
if (empty($this->userdata)) { if (empty($this->userdata)) {
$this->userdata = new $userdataclassname(); $this->userdata = new $userdataclassname();
} }
$result = $this->userdata->getUserData($key, $this->cas->getUser()); return $this->userdata->getUserData($key, $this->cas->getUser());
return $result;
} }
function logout(){ function logout()
{
// do a global cas logout // do a global cas logout
$this->cas = new CAS_Client(CAS_VERSION_2_0, false, $this->host, $this->port, $this->uri, false); $this->cas = new CAS_Client(CAS_VERSION_2_0, false, $this->host, $this->port, $this->uri, false);
$this->cas->logout(); $this->cas->logout();
......
<?php <?php
# Lifter007: TODO
# Lifter003: TODO
# Lifter010: TODO
// +---------------------------------------------------------------------------+ // +---------------------------------------------------------------------------+
// This file is part of Stud.IP // This file is part of Stud.IP
// StudipAuthLdap.class.php // StudipAuthLdap.class.php
...@@ -33,31 +30,19 @@ ...@@ -33,31 +30,19 @@
* @author André Noack <noack@data-quest.de> * @author André Noack <noack@data-quest.de>
* @package * @package
*/ */
class StudipAuthLdap extends StudipAuthAbstract { class StudipAuthLdap extends StudipAuthAbstract
{
var $anonymous_bind = true; public $anonymous_bind = true;
var $host; public $host;
var $base_dn; public $base_dn;
var $username_attribute = 'uid'; public $username_attribute = 'uid';
var $ldap_filter; public $ldap_filter;
var $bad_char_regex = '/[^0-9_a-zA-Z]/'; public $bad_char_regex = '/[^0-9_a-zA-Z]/';
var $conn = null; public $conn = null;
var $user_data = null; public $user_data = null;
/**
* Constructor
*
*
* @access public
*
*/
function __construct()
{
//calling the baseclass constructor
parent::__construct();
}
function getLdapFilter($username) function getLdapFilter($username)
...@@ -76,16 +61,16 @@ class StudipAuthLdap extends StudipAuthAbstract { ...@@ -76,16 +61,16 @@ class StudipAuthLdap extends StudipAuthAbstract {
function doLdapConnect() function doLdapConnect()
{ {
if (!($this->conn = ldap_connect($this->host))) { if (!($this->conn = ldap_connect($this->host))) {
$this->error_msg = _("Keine Verbindung zum LDAP Server möglich."); $this->error_msg = _('Keine Verbindung zum LDAP Server möglich.');
return false; return false;
} }
if (!($r = ldap_set_option($this->conn, LDAP_OPT_PROTOCOL_VERSION, 3))) { if (!($r = ldap_set_option($this->conn, LDAP_OPT_PROTOCOL_VERSION, 3))) {
$this->error_msg = _("Setzen der LDAP Protokolversion fehlgeschlagen."); $this->error_msg = _('Setzen der LDAP Protokolversion fehlgeschlagen.');
return false; return false;
} }
if ($this->start_tls) { if ($this->start_tls) {
if (!ldap_start_tls($this->conn)) { if (!ldap_start_tls($this->conn)) {
$this->error_msg = _("\"Start TLS\" fehlgeschlagen."); $this->error_msg = _('"Start TLS" fehlgeschlagen.');
return false; return false;
} }
} }
...@@ -94,19 +79,19 @@ class StudipAuthLdap extends StudipAuthAbstract { ...@@ -94,19 +79,19 @@ class StudipAuthLdap extends StudipAuthAbstract {
function getUserDn($username) function getUserDn($username)
{ {
$user_dn = ""; $user_dn = '';
if ($this->anonymous_bind) { if ($this->anonymous_bind) {
if (!($r = @ldap_bind($this->conn))) { if (!($r = @ldap_bind($this->conn))) {
$this->error_msg =_("Anonymer Bind fehlgeschlagen.") . $this->getLdapError(); $this->error_msg = _('Anonymer Bind fehlgeschlagen.') . $this->getLdapError();
return false; return false;
} }
if (!($result = @ldap_search($this->conn, $this->base_dn, $this->getLdapFilter($username), ['dn']))) { if (!($result = @ldap_search($this->conn, $this->base_dn, $this->getLdapFilter($username), ['dn']))) {
$this->error_msg = _("Anonymes Durchsuchen des LDAP Baumes fehlgeschlagen.") .$this->getLdapError(); $this->error_msg = _('Anonymes Durchsuchen des LDAP Baumes fehlgeschlagen.') . $this->getLdapError();
return false; return false;
} }
if (!ldap_count_entries($this->conn, $result)) { if (!ldap_count_entries($this->conn, $result)) {
$this->error_msg = sprintf(_("%s wurde nicht unterhalb von %s gefunden."), $username, $this->base_dn); $this->error_msg = sprintf(_('%s wurde nicht unterhalb von %s gefunden.'), $username, $this->base_dn);
return false; return false;
} }
if (!($entry = @ldap_first_entry($this->conn, $result))) { if (!($entry = @ldap_first_entry($this->conn, $result))) {
...@@ -118,7 +103,7 @@ class StudipAuthLdap extends StudipAuthAbstract { ...@@ -118,7 +103,7 @@ class StudipAuthLdap extends StudipAuthAbstract {
return false; return false;
} }
} else { } else {
$user_dn = $this->username_attribute . "=" . $username . "," . $this->base_dn; $user_dn = $this->username_attribute . '=' . $username . ',' . $this->base_dn;
} }
return $user_dn; return $user_dn;
} }
...@@ -132,18 +117,18 @@ class StudipAuthLdap extends StudipAuthAbstract { ...@@ -132,18 +117,18 @@ class StudipAuthLdap extends StudipAuthAbstract {
return false; return false;
} }
if (!$password) { if (!$password) {
$this->error_msg = _("Kein Passwort eingegeben."); //some ldap servers seem to allow binding with a user dn and without a password, if anonymous bind is enabled $this->error_msg = _('Kein Passwort eingegeben.'); //some ldap servers seem to allow binding with a user dn and without a password, if anonymous bind is enabled
return false; return false;
} }
if (!($r = @ldap_bind($this->conn, $user_dn, $password))) { if (!($r = @ldap_bind($this->conn, $user_dn, $password))) {
if (ldap_errno($this->conn) == 49) { if (ldap_errno($this->conn) == 49) {
$this->error_msg = _("Bitte überprüfen Sie ihre Zugangsdaten."); $this->error_msg = _('Bitte überprüfen Sie ihre Zugangsdaten.');
} }
$this->error_msg = _("Anmeldung fehlgeschlagen.") . $this->getLdapError(); $this->error_msg = _('Anmeldung fehlgeschlagen.') . $this->getLdapError();
return false; return false;
} }
if (!($result = @ldap_search($this->conn, $user_dn, "objectclass=*"))){ if (!($result = @ldap_search($this->conn, $user_dn, 'objectclass=*'))) {
$this->error_msg = _("Abholen der Benutzer Attribute fehlgeschlagen.") .$this->getLdapError(); $this->error_msg = _('Abholen der Benutzer Attribute fehlgeschlagen.') . $this->getLdapError();
return false; return false;
} }
if (@ldap_count_entries($this->conn, $result)) { if (@ldap_count_entries($this->conn, $result)) {
...@@ -174,12 +159,11 @@ class StudipAuthLdap extends StudipAuthAbstract { ...@@ -174,12 +159,11 @@ class StudipAuthLdap extends StudipAuthAbstract {
} }
function doLdapMap($map_params) function doLdapMap($map_params)
{ {
if (isset($this->user_data[$map_params][0])) { if (isset($this->user_data[$map_params][0])) {
$ret = $this->user_data[$map_params][0]; $ret = $this->user_data[$map_params][0];
if ($ret[0] == ':') { if ($ret[0] === ':') {
$ret = base64_decode($ret); $ret = base64_decode($ret);
} }
} }
...@@ -203,22 +187,22 @@ class StudipAuthLdap extends StudipAuthAbstract { ...@@ -203,22 +187,22 @@ class StudipAuthLdap extends StudipAuthAbstract {
function isUsedUsername($username) function isUsedUsername($username)
{ {
if (!$this->anonymous_bind) { if (!$this->anonymous_bind) {
$this->error = _("Kann den Benutzernamen nicht überprüfen, anonymous_bind ist ausgeschaltet!"); $this->error = _('Kann den Benutzernamen nicht überprüfen, anonymous_bind ist ausgeschaltet!');
return false; return false;
} }
if (!$this->doLdapConnect()) { if (!$this->doLdapConnect()) {
return false; return false;
} }
if (!($r = @ldap_bind($this->conn))) { if (!($r = @ldap_bind($this->conn))) {
$this->error = _("Anonymer Bind fehlgeschlagen.") . $this->getLdapError(); $this->error = _('Anonymer Bind fehlgeschlagen.') . $this->getLdapError();
return false; return false;
} }
if (!($result = @ldap_search($this->conn, $this->base_dn, $this->getLdapFilter($username), ['dn']))) { if (!($result = @ldap_search($this->conn, $this->base_dn, $this->getLdapFilter($username), ['dn']))) {
$this->error = _("Anonymes Durchsuchen des LDAP Baumes fehlgeschlagen.") .$this->getLdapError(); $this->error = _('Anonymes Durchsuchen des LDAP Baumes fehlgeschlagen.') . $this->getLdapError();
return false; return false;
} }
if (!ldap_count_entries($this->conn, $result)) { if (!ldap_count_entries($this->conn, $result)) {
$this->error_msg = _("Der Benutzername wurde nicht gefunden."); $this->error_msg = _('Der Benutzername wurde nicht gefunden.');
return false; return false;
} }
return true; return true;
...@@ -226,6 +210,6 @@ class StudipAuthLdap extends StudipAuthAbstract { ...@@ -226,6 +210,6 @@ class StudipAuthLdap extends StudipAuthAbstract {
function getLdapError() function getLdapError()
{ {
return _("<br>LDAP Fehler: ") . ldap_error($this->conn) ." (#" . ldap_errno($this->conn) . ")"; return _('<br>LDAP Fehler: ') . ldap_error($this->conn) . ' (#' . ldap_errno($this->conn) . ')';
} }
} }
...@@ -35,38 +35,27 @@ ...@@ -35,38 +35,27 @@
* @author André Noack <noack@data-quest.de> * @author André Noack <noack@data-quest.de>
* @package * @package
*/ */
class StudipAuthLdapReadAndBind extends StudipAuthLdap { class StudipAuthLdapReadAndBind extends StudipAuthLdap
{
var $anonymous_bind = false; public $anonymous_bind = false;
var $reader_dn; public $reader_dn;
var $reader_password; public $reader_password;
/** function getUserDn($username)
* Constructor {
* $user_dn = '';
*
* @access public
*
*/
function __construct() {
//calling the baseclass constructor
parent::__construct();
}
function getUserDn($username){
$user_dn = "";
if (!($r = @ldap_bind($this->conn, $this->reader_dn, $this->reader_password))) { if (!($r = @ldap_bind($this->conn, $this->reader_dn, $this->reader_password))) {
$this->error_msg = sprintf(_("Anmeldung von %s fehlgeschlagen."),$this->reader_dn) . $this->getLdapError(); $this->error_msg = sprintf(_('Anmeldung von %s fehlgeschlagen.'), $this->reader_dn) . $this->getLdapError();
return false; return false;
} }
if (!($result = @ldap_search($this->conn, $this->base_dn, $this->getLdapFilter($username), ['dn']))) { if (!($result = @ldap_search($this->conn, $this->base_dn, $this->getLdapFilter($username), ['dn']))) {
$this->error_msg = _("Durchsuchen des LDAP Baumes fehlgeschlagen.") .$this->getLdapError(); $this->error_msg = _('Durchsuchen des LDAP Baumes fehlgeschlagen.') . $this->getLdapError();
return false; return false;
} }
if (!ldap_count_entries($this->conn, $result)) { if (!ldap_count_entries($this->conn, $result)) {
$this->error_msg = sprintf(_("%s wurde nicht unterhalb von %s gefunden."), $username, $this->base_dn); $this->error_msg = sprintf(_('%s wurde nicht unterhalb von %s gefunden.'), $username, $this->base_dn);
return false; return false;
} }
if (!($entry = @ldap_first_entry($this->conn, $result))) { if (!($entry = @ldap_first_entry($this->conn, $result))) {
...@@ -80,7 +69,8 @@ class StudipAuthLdapReadAndBind extends StudipAuthLdap { ...@@ -80,7 +69,8 @@ class StudipAuthLdapReadAndBind extends StudipAuthLdap {
return $user_dn; return $user_dn;
} }
function isUsedUsername($username){ function isUsedUsername($username)
{
if (!$this->doLdapConnect()) { if (!$this->doLdapConnect()) {
return false; return false;
} }
......
<?php
/*
* StudipAuthOpenID.class.php - Stud.IP authentication using OpenID Connect
* Copyright (c) 2021 André Noack <noack@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 (at your option) any later version.
*/
use Jumbojett\OpenIDConnectClient;
use Jumbojett\OpenIDConnectClientException;
class StudipAuthOIDC extends StudipAuthSSO
{
/**
* @var OpenIDConnectClient
*/
private $oidc;
/**
* @var string
*/
public $provider_url;
/**
* @var string
*/
public $client_id;
/**
* @var string
*/
public $client_secret;
/**
* @param array $config
*/
public function __construct($config = [])
{
parent::__construct($config);
if (Request::get('sso') === $this->plugin_name) {
$this->oidc = new OpenIDConnectClient($this->provider_url, $this->client_id, $this->client_secret);
if (isset($this->ssl_options)) {
foreach ($this->ssl_options as $option_key => $option_value) {
if (isset($option_value)) {
$this->oidc->{'set' . $option_key}($option_value);
}
}
if (Config::get()->HTTP_PROXY) {
$this->oidc->setHttpProxy(Config::get()->HTTP_PROXY);
}
$return_url = URLHelper::getScriptURL($GLOBALS['ABSOLUTE_URI_STUDIP'] . 'index.php', ['sso' => $this->plugin_name, 'again' => 'yes']);
$this->oidc->setRedirectURL($return_url);
$this->oidc->addScope(['openid', 'email', 'profile']);
}
}
}
/**
* Validate the username passed to the auth plugin.
*
* @param string $username
*
* @return string username openid attribute user_id@domain
*
* @throws OpenIDConnectClientException
*/
public function verifyUsername($username)
{
$this->oidc->authenticate();
$this->userdata = (array)$this->oidc->requestUserInfo();
if (isset($this->userdata['sub'])) {
return $this->userdata['username'] = $this->userdata['sub'] . '@' . $this->domain;
} else {
return null;
}
}
/**
* Return the current username of the pending authentication request.
*/
public function getUser()
{
return $this->userdata['username'];
}
/**
* Get the user domains to assign to the current user (if any).
*
* @return array array of user domain names
*/
public function getUserDomains()
{
return $this->domain ? [$this->domain] : null;
}
/**
* Callback that can be used in user_data_mapping array.
*
* @see https://openid.net/specs/openid-connect-basic-1_0.html#StandardClaims
*
* @param string key
*
* @return string parameter value (null if not set)
*/
public function getUserData($key)
{
return $this->userdata[$key];
}
}
...@@ -17,6 +17,16 @@ ...@@ -17,6 +17,16 @@
*/ */
abstract class StudipAuthSSO extends StudipAuthAbstract abstract class StudipAuthSSO extends StudipAuthAbstract
{ {
/**
* @var string the descriptive name of the authentication plugin
*/
public $plugin_fullname;
/**
* @var string a short description, when present it is shown on the login page
*/
public $login_description;
/** /**
* Return the current username. * Return the current username.
*/ */
......
...@@ -14,25 +14,36 @@ ...@@ -14,25 +14,36 @@
class StudipAuthShib extends StudipAuthSSO class StudipAuthShib extends StudipAuthSSO
{ {
var $env_remote_user = 'HTTP_REMOTE_USER'; public $env_remote_user = 'HTTP_REMOTE_USER';
var $local_domain; public $local_domain;
var $session_initiator; public $session_initiator;
var $validate_url; public $validate_url;
var $userdata; public $userdata;
public $username_attribute = 'username';
/** /**
* Constructor: read auth information from remote SP. * Constructor: read auth information from remote SP.
*/ */
function __construct() public function __construct($config = [])
{ {
parent::__construct(); parent::__construct($config);
if (Request::option('sso') && isset($this->validate_url) && isset($_REQUEST['token'])) { if (!isset($this->plugin_fullname)) {
$this->plugin_fullname = _('Shibboleth');
}
if (!isset($this->login_description)) {
$this->login_description = _('für Single Sign On mit Shibboleth');
}
if (Request::get('sso') === $this->plugin_name && isset($this->validate_url) && isset($_REQUEST['token'])) {
$context = get_default_http_stream_context($this->validate_url); $context = get_default_http_stream_context($this->validate_url);
$auth = file_get_contents($this->validate_url . '/' . $_REQUEST['token'], false, $context); $auth = file_get_contents($this->validate_url . '/' . $_REQUEST['token'], false, $context);
$this->userdata = json_decode($auth, true); $this->userdata = json_decode($auth, true);
if ($this->username_attribute !== 'username') {
$this->userdata['username'] = $this->userdata[$this->username_attribute];
}
if (isset($this->local_domain)) { if (isset($this->local_domain)) {
$this->userdata['username'] = $this->userdata['username'] =
str_replace('@' . $this->local_domain, '', $this->userdata['username']); str_replace('@' . $this->local_domain, '', $this->userdata['username']);
...@@ -89,14 +100,14 @@ class StudipAuthShib extends StudipAuthSSO ...@@ -89,14 +100,14 @@ class StudipAuthShib extends StudipAuthSSO
} }
if (empty($remote_user) || isset($this->validate_url)) { if (empty($remote_user) || isset($this->validate_url)) {
if ($_REQUEST['sso'] == 'shib') { if (Request::get('sso') === $this->plugin_name) {
// force Shibboleth authentication (lazy session) // force Shibboleth authentication (lazy session)
$shib_url = $this->session_initiator; $shib_url = $this->session_initiator;
$shib_url .= mb_strpos($shib_url, '?') === false ? '?' : '&'; $shib_url .= strpos($shib_url, '?') === false ? '?' : '&';
$shib_url .= 'target=' . urlencode($this->getURL()); $shib_url .= 'target=' . urlencode($this->getURL());
// break redirection loop in case of misconfiguration // break redirection loop in case of misconfiguration
if (mb_strstr($_SERVER['HTTP_REFERER'], 'target=') == false) { if (strstr($_SERVER['HTTP_REFERER'], 'target=') === false) {
header('Location: ' . $shib_url); header('Location: ' . $shib_url);
echo '<html></html>'; echo '<html></html>';
exit(); exit();
...@@ -107,10 +118,6 @@ class StudipAuthShib extends StudipAuthSSO ...@@ -107,10 +118,6 @@ class StudipAuthShib extends StudipAuthSSO
return NULL; return NULL;
} }
if (isset($this->local_domain)) {
$remote_user = str_replace('@'.$this->local_domain, '', $remote_user);
}
// import authentication information // import authentication information
$this->userdata['username'] = $remote_user; $this->userdata['username'] = $remote_user;
...@@ -121,6 +128,13 @@ class StudipAuthShib extends StudipAuthSSO ...@@ -121,6 +128,13 @@ class StudipAuthShib extends StudipAuthSSO
} }
} }
if ($this->username_attribute !== 'username') {
$this->userdata['username'] = $this->userdata[$this->username_attribute];
}
if (isset($this->local_domain)) {
$this->userdata['username'] =
str_replace('@' . $this->local_domain, '', $this->userdata['username']);
}
return $this->getUser(); return $this->getUser();
} }
......
...@@ -38,18 +38,6 @@ class StudipAuthStandard extends StudipAuthAbstract ...@@ -38,18 +38,6 @@ class StudipAuthStandard extends StudipAuthAbstract
var $bad_char_regex = false; var $bad_char_regex = false;
/**
* Constructor
*
*
* @access public
*
*/
function __construct()
{
parent::__construct();
}
/** /**
* *
* *
...@@ -61,19 +49,19 @@ class StudipAuthStandard extends StudipAuthAbstract ...@@ -61,19 +49,19 @@ class StudipAuthStandard extends StudipAuthAbstract
{ {
$user = User::findByUsername($username); $user = User::findByUsername($username);
if (!$user || !$password || mb_strlen($password) > 72) { if (!$user || !$password || mb_strlen($password) > 72) {
$this->error_msg= _("Ungültige Benutzername/Passwort-Kombination!") ; $this->error_msg= _('Ungültige Benutzername/Passwort-Kombination!') ;
return false; return false;
} elseif ($user->username != $username) { } elseif ($user->username !== $username) {
$this->error_msg = _("Bitte achten Sie auf korrekte Gro&szlig;-Kleinschreibung beim Username!"); $this->error_msg = _('Bitte achten Sie auf korrekte Groß-Kleinschreibung beim Username!');
return false; return false;
} elseif (!is_null($user->auth_plugin) && $user->auth_plugin != "standard") { } elseif (!is_null($user->auth_plugin) && $user->auth_plugin !== 'standard') {
$this->error_msg = sprintf(_("Dieser Benutzername wird bereits über %s authentifiziert!"),$user->auth_plugin) ; $this->error_msg = sprintf(_('Dieser Benutzername wird bereits über %s authentifiziert!'),$user->auth_plugin) ;
return false; return false;
} else { } else {
$pass = $user->password; // Password is stored as a md5 hash $pass = $user->password; // Password is stored as a md5 hash
} }
$hasher = UserManagement::getPwdHasher(); $hasher = UserManagement::getPwdHasher();
$old_style_check = (mb_strlen($pass) == 32 && md5($password) == $pass); $old_style_check = (strlen($pass) === 32 && md5($password) === $pass);
$migrated_check = $hasher->CheckPassword(md5($password), $pass); $migrated_check = $hasher->CheckPassword(md5($password), $pass);
$check = $hasher->CheckPassword($password, $pass); $check = $hasher->CheckPassword($password, $pass);
$old_encoding_check = $hasher->CheckPassword(legacy_studip_utf8decode($password), $pass); $old_encoding_check = $hasher->CheckPassword(legacy_studip_utf8decode($password), $pass);
...@@ -85,7 +73,7 @@ class StudipAuthStandard extends StudipAuthAbstract ...@@ -85,7 +73,7 @@ class StudipAuthStandard extends StudipAuthAbstract
} }
if (!($check || $migrated_check || $old_style_check || $old_encoding_check)) { if (!($check || $migrated_check || $old_style_check || $old_encoding_check)) {
$this->error_msg= _("Das Passwort ist falsch!"); $this->error_msg= _('Das Passwort ist falsch!');
return false; return false;
} else { } else {
return true; return true;
......
...@@ -27,16 +27,12 @@ class LoginNavigation extends Navigation ...@@ -27,16 +27,12 @@ class LoginNavigation extends Navigation
$navigation->setDescription(_('für registrierte NutzerInnen')); $navigation->setDescription(_('für registrierte NutzerInnen'));
$this->addSubNavigation('login', $navigation); $this->addSubNavigation('login', $navigation);
if (in_array('CAS', $GLOBALS['STUDIP_AUTH_PLUGIN'])) { foreach (StudipAuthAbstract::getInstance() as $auth_plugin) {
$navigation = new Navigation(_('Login'), 'index.php?again=yes&sso=cas'); if ($auth_plugin instanceof StudipAuthSSO && isset($auth_plugin->login_description)) {
$navigation->setDescription(_('für Single Sign On mit CAS')); $navigation = new Navigation($auth_plugin->plugin_fullname . ' ' . _('Login'), 'index.php?again=yes&sso=' . $auth_plugin->plugin_name);
$this->addSubNavigation('login_cas', $navigation); $navigation->setDescription($auth_plugin->login_description);
$this->addSubNavigation('login_' . $auth_plugin->plugin_name, $navigation);
} }
if (in_array('Shib', $GLOBALS['STUDIP_AUTH_PLUGIN'])) {
$navigation = new Navigation(_('Shibboleth Login'), 'index.php?again=yes&sso=shib');
$navigation->setDescription(_('für Single Sign On mit Shibboleth'));
$this->addSubNavigation('login_shib', $navigation);
} }
if (Config::get()->ENABLE_SELF_REGISTRATION) { if (Config::get()->ENABLE_SELF_REGISTRATION) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment