diff --git a/composer.json b/composer.json index 7635f33f31..ad3bd6aa47 100644 --- a/composer.json +++ b/composer.json @@ -23,10 +23,8 @@ "yiisoft/yii2-jui": "^2.0", "zendframework/zend-http": "*", "nqxcode/zendsearch": "^2.0", - "xj/yii2-jplayer-widget": "*" - }, - "suggest": { - "zendframework/zend-ldap": "LDAP authentication" + "xj/yii2-jplayer-widget": "*", + "zendframework/zend-ldap": "^2.5" }, "require-dev": { "yiisoft/yii2-codeception": "*", diff --git a/protected/humhub/components/Module.php b/protected/humhub/components/Module.php index 1096d98d2a..7ecfed7d91 100644 --- a/protected/humhub/components/Module.php +++ b/protected/humhub/components/Module.php @@ -21,17 +21,10 @@ class Module extends \yii\base\Module { /** - * Loaded Module JSON File - * - * @var Array + * @var Array the loaded module.json info file */ private $_moduleInfo = null; - /** - * Config Route - */ - public $configRoute = null; - /** * The path for module resources (images, javascripts) * Also module related assets like README.md and module_image.png should be placed here. @@ -138,10 +131,9 @@ class Module extends \yii\base\Module */ public function disable() { - - // Seems not enabled + // Is not enabled if (!Yii::$app->hasModule($this->id)) { - return false; + return; } // Disable module in database @@ -182,6 +174,7 @@ class Module extends \yii\base\Module } } + /* HSetting::model()->deleteAllByAttributes(array('module_id' => $this->getId())); SpaceSetting::model()->deleteAllByAttributes(array('module_id' => $this->getId())); @@ -199,7 +192,6 @@ class Module extends \yii\base\Module ModuleManager::flushCache(); */ - return true; } /** diff --git a/protected/humhub/docs/guide/dev-migrate-0.20-status.md b/protected/humhub/docs/guide/dev-migrate-0.20-status.md index 0e7e7ab8f0..6d8e886c0c 100644 --- a/protected/humhub/docs/guide/dev-migrate-0.20-status.md +++ b/protected/humhub/docs/guide/dev-migrate-0.20-status.md @@ -2,16 +2,11 @@ # HumHub 0.20 - Status -## Open - -- LDAP - ## Bugs / ToDos / To Improve - Installer mac - Check complexer Migrations - ComandLine Space Tool -- Check also writable config file ## Modules diff --git a/protected/humhub/libs/SelfTest.php b/protected/humhub/libs/SelfTest.php index 8cd7d569bb..b1a5151fbb 100644 --- a/protected/humhub/libs/SelfTest.php +++ b/protected/humhub/libs/SelfTest.php @@ -148,8 +148,8 @@ class SelfTest } // Checks LDAP Extension - $title = 'PHP - LDAP Support'; - if (function_exists('ldap_bind')) { + $title = 'LDAP Support'; + if (\humhub\modules\user\libs\Ldap::isAvailable()) { $checks[] = array( 'title' => Yii::t('base', $title), 'state' => 'OK' @@ -158,7 +158,7 @@ class SelfTest $checks[] = array( 'title' => Yii::t('base', $title), 'state' => 'WARNING', - 'hint' => 'Optional - Install LDAP Extension for LDAP Support' + 'hint' => 'Optional - Install PHP LDAP Extension and Zend LDAP Composer Package' ); } @@ -194,21 +194,21 @@ class SelfTest // Checks Writeable Config /* - $title = 'Permissions - Config'; - $configFile = dirname(Yii::$app->params['dynamicConfigFile']); - if (is_writeable($configFile)) { - $checks[] = array( - 'title' => Yii::t('base', $title), - 'state' => 'OK' - ); - } else { - $checks[] = array( - 'title' => Yii::t('base', $title), - 'state' => 'ERROR', - 'hint' => 'Make ' . $configFile . " writable for the webserver/php!" - ); - } - */ + $title = 'Permissions - Config'; + $configFile = dirname(Yii::$app->params['dynamicConfigFile']); + if (is_writeable($configFile)) { + $checks[] = array( + 'title' => Yii::t('base', $title), + 'state' => 'OK' + ); + } else { + $checks[] = array( + 'title' => Yii::t('base', $title), + 'state' => 'ERROR', + 'hint' => 'Make ' . $configFile . " writable for the webserver/php!" + ); + } + */ // Check Runtime Directory $title = 'Permissions - Runtime'; @@ -274,7 +274,21 @@ class SelfTest 'hint' => 'Make ' . $path . " writable for the webserver/php!" ); } - + // Check Custom Modules Directory + $title = 'Permissions - Dynamic Config'; + $path = Yii::getAlias(Yii::$app->params['dynamicConfigFile']); + if (is_writeable($path)) { + $checks[] = array( + 'title' => Yii::t('base', $title), + 'state' => 'OK' + ); + } else { + $checks[] = array( + 'title' => Yii::t('base', $title), + 'state' => 'ERROR', + 'hint' => 'Make ' . $path . " writable for the webserver/php!" + ); + } return $checks; } diff --git a/protected/humhub/modules/admin/controllers/SettingController.php b/protected/humhub/modules/admin/controllers/SettingController.php index 95f842057e..6967f3d119 100644 --- a/protected/humhub/modules/admin/controllers/SettingController.php +++ b/protected/humhub/modules/admin/controllers/SettingController.php @@ -13,6 +13,7 @@ use yii\helpers\Url; use humhub\models\Setting; use humhub\models\UrlOembed; use humhub\modules\admin\components\Controller; +use humhub\modules\user\libs\Ldap; /** * SettingController @@ -184,20 +185,20 @@ class SettingController extends Controller $userCount = 0; $errorMessage = ""; - /* - if (Setting::Get('enabled', 'authentication_ldap')) { - $enabled = true; - try { - if (HLdap::getInstance()->ldap !== null) { - $userCount = HLdap::getInstance()->ldap->count(Setting::Get('userFilter', 'authentication_ldap'), Setting::Get('baseDn', 'authentication_ldap'), Zend_Ldap::SEARCH_SCOPE_SUB); - } else { - $errorMessage = Yii::t('AdminModule.controllers_SettingController', 'Could not load LDAP! - Check PHP Extension'); - } - } catch (Exception $ex) { - $errorMessage = $ex->getMessage(); - } - } - */ + if (Setting::Get('enabled', 'authentication_ldap')) { + $enabled = true; + try { + if (Ldap::getInstance()->ldap !== null) { + $userCount = Ldap::getInstance()->ldap->count(Setting::Get('userFilter', 'authentication_ldap'), Setting::Get('baseDn', 'authentication_ldap'), \Zend\Ldap\Ldap::SEARCH_SCOPE_SUB); + } else { + $errorMessage = Yii::t('AdminModule.controllers_SettingController', 'Could not load LDAP! - Check PHP Extension'); + } + } catch (\Zend\Ldap\Exception\LdapException $ex) { + $errorMessage = $ex->getMessage(); + } catch (Exception $ex) { + $errorMessage = $ex->getMessage(); + } + } return $this->render('authentication_ldap', array('model' => $form, 'enabled' => $enabled, 'userCount' => $userCount, 'errorMessage' => $errorMessage)); } diff --git a/protected/humhub/modules/admin/views/setting/authentication.php b/protected/humhub/modules/admin/views/setting/authentication.php index 46bd7ea20f..d0c82d0632 100644 --- a/protected/humhub/modules/admin/views/setting/authentication.php +++ b/protected/humhub/modules/admin/views/setting/authentication.php @@ -13,9 +13,11 @@ use yii\helpers\Url;
  • -
  • - -
  • + +
  • + +
  • + diff --git a/protected/humhub/modules/search/config.php b/protected/humhub/modules/search/config.php index 0a36f31832..040542a7d7 100644 --- a/protected/humhub/modules/search/config.php +++ b/protected/humhub/modules/search/config.php @@ -13,7 +13,6 @@ return [ ['class' => TopMenuRightStack::className(), 'event' => TopMenuRightStack::EVENT_INIT, 'callback' => array(Events::className(), 'onTopMenuRightInit')], ['class' => Application::className(), 'event' => Application::EVENT_ON_INIT, 'callback' => array(Events::className(), 'onConsoleApplicationInit')], ['class' => CronController::className(), 'event' => CronController::EVENT_ON_HOURLY_RUN, 'callback' => [Events::className(), 'onHourlyCron']], - //array('class' => 'Comment', 'event' => 'onAfterSave', 'callback' => array('SearchModuleEvents', 'onAfterSaveComment')), ), ]; ?> \ No newline at end of file diff --git a/protected/humhub/modules/user/Events.php b/protected/humhub/modules/user/Events.php index d723c60991..de7679bc30 100644 --- a/protected/humhub/modules/user/Events.php +++ b/protected/humhub/modules/user/Events.php @@ -8,6 +8,8 @@ use humhub\modules\user\models\Password; use humhub\modules\user\models\Profile; use humhub\modules\user\models\Mentioning; use humhub\modules\user\models\Follow; +use humhub\modules\user\libs\Ldap; +use humhub\models\Setting; /** * Events provides callbacks for all defined module events. @@ -38,7 +40,6 @@ class Events extends \yii\base\Object { models\Mentioning::deleteAll(['object_model' => $event->sender->className(), 'object_id' => $event->sender->getPrimaryKey()]); models\Follow::deleteAll(['object_model' => $event->sender->className(), 'object_id' => $event->sender->getPrimaryKey()]); - } /** @@ -125,4 +126,15 @@ class Events extends \yii\base\Object } } + public static function onHourlyCron($event) + { + $controller = $event->sender; + + if (Setting::Get('enabled', 'authentication_ldap') && Setting::Get('refreshUsers', 'authentication_ldap') && Ldap::isAvailable()) { + $controller->stdout("Refresh ldap users... "); + Ldap::getInstance()->refreshUsers(); + $controller->stdout('done.' . PHP_EOL, \yii\helpers\Console::FG_GREEN); + } + } + } diff --git a/protected/humhub/modules/user/config.php b/protected/humhub/modules/user/config.php index bfed182c67..66469052ad 100644 --- a/protected/humhub/modules/user/config.php +++ b/protected/humhub/modules/user/config.php @@ -5,6 +5,7 @@ use humhub\modules\user\Events; use humhub\commands\IntegrityController; use humhub\modules\content\components\ContentAddonActiveRecord; use humhub\modules\content\components\ContentActiveRecord; +use humhub\commands\CronController; return [ 'id' => 'user', @@ -13,11 +14,12 @@ return [ 'urlManagerRules' => [ ['class' => 'humhub\modules\user\components\UrlRule'] ], - 'events' => array( - array('class' => Search::className(), 'event' => Search::EVENT_ON_REBUILD, 'callback' => array(Events::className(), 'onSearchRebuild')), - array('class' => ContentActiveRecord::className(), 'event' => ContentActiveRecord::EVENT_BEFORE_DELETE, 'callback' => array(Events::className(), 'onContentDelete')), - array('class' => ContentAddonActiveRecord::className(), 'event' => ContentAddonActiveRecord::EVENT_BEFORE_DELETE, 'callback' => array(Events::className(), 'onContentDelete')), - array('class' => IntegrityController::className(), 'event' => IntegrityController::EVENT_ON_RUN, 'callback' => array(Events::className(), 'onIntegrityCheck')), - ) + 'events' => [ + ['class' => Search::className(), 'event' => Search::EVENT_ON_REBUILD, 'callback' => array(Events::className(), 'onSearchRebuild')], + ['class' => ContentActiveRecord::className(), 'event' => ContentActiveRecord::EVENT_BEFORE_DELETE, 'callback' => array(Events::className(), 'onContentDelete')], + ['class' => ContentAddonActiveRecord::className(), 'event' => ContentAddonActiveRecord::EVENT_BEFORE_DELETE, 'callback' => array(Events::className(), 'onContentDelete')], + ['class' => IntegrityController::className(), 'event' => IntegrityController::EVENT_ON_RUN, 'callback' => array(Events::className(), 'onIntegrityCheck')], + ['class' => CronController::className(), 'event' => CronController::EVENT_ON_HOURLY_RUN, 'callback' => [Events::className(), 'onHourlyCron']], + ] ]; ?> \ No newline at end of file diff --git a/protected/humhub/modules/user/libs/LDAP.php b/protected/humhub/modules/user/libs/LDAP.php new file mode 100644 index 0000000000..cfcb42c327 --- /dev/null +++ b/protected/humhub/modules/user/libs/LDAP.php @@ -0,0 +1,275 @@ + Setting::Get('hostname', 'authentication_ldap'), + 'port' => Setting::Get('port', 'authentication_ldap'), + 'username' => Setting::Get('username', 'authentication_ldap'), + 'password' => Setting::Get('password', 'authentication_ldap'), + 'useStartTls' => (Setting::Get('encryption', 'authentication_ldap') == 'tls'), + 'useSsl' => (Setting::Get('encryption', 'authentication_ldap') == 'ssl'), + 'bindRequiresDn' => true, + 'baseDn' => Setting::Get('baseDn', 'authentication_ldap'), + 'accountFilterFormat' => Setting::Get('loginFilter', 'authentication_ldap'), + ); + + $this->ldap = new \Zend\Ldap\Ldap($options); + $this->ldap->bind(); + } catch (\Zend\Ldap\Exception\LdapException $ex) { + Yii::error('Cound not bind to LDAP Server. Error: ' . $ex->getMessage()); + } catch (Exception $ex) { + Yii::error('Cound not bind to LDAP Server. Error: ' . $ex->getMessage()); + } + } + + /** + * Authenticates user against LDAP Backend + * + * @param type $username + * @param type $password + * @return boolean + */ + public function authenticate($username, $password) + { + $username = $this->ldap->getCanonicalAccountName($username, \Zend\Ldap\Ldap::ACCTNAME_FORM_DN); + try { + $this->ldap->bind($username, $password); + + // Update Users Data + $node = $this->ldap->getNode($username); + $this->handleLdapUser($node); + + return true; + } catch (Exception $ex) { + return false; + } + return false; + } + + /** + * Reads out all users from configured ldap backend and creates or update + * existing users. + * + * Also disabling deleted ldap users in humhub + */ + public function refreshUsers() + { + + $ldapUserIds = array(); + + try { + $items = $this->ldap->search(Setting::Get('userFilter', 'authentication_ldap'), Setting::Get('baseDn', 'authentication_ldap'), \Zend\Ldap\Ldap::SEARCH_SCOPE_SUB); + foreach ($items as $item) { + $node = \Zend\Ldap\Node::fromArray($item); + $user = $this->handleLdapUser($node); + + if ($user != null) + $ldapUserIds[] = $user->id; + } + + + foreach (User::find()->where(['auth_mode' => User::AUTH_MODE_LDAP])->andWhere(['!=', 'status', User::STATUS_DISABLED])->each() as $user) { + if (!in_array($user->id, $ldapUserIds)) { + // User no longer available in ldap + $user->status = User::STATUS_DISABLED; + $user->save(); + Yii::warning('Disabled user ' . $user->username . ' (' . $user->id . ') - Not found in LDAP!'); + } + } + } catch (Exception $ex) { + Yii::error($ex->getMessage()); + } + } + + /** + * Updates or creates user by given ldap node + * + * @param Zend_Ldap_Node $node + * @return User User Object + */ + public function handleLdapUser($node) + { + + $username = $node->getAttribute(Setting::Get('usernameAttribute', 'authentication_ldap'), 0); + $email = $node->getAttribute('mail', 0); + $guid = $this->binToStrGuid($node->getAttribute('objectGUID', 0)); + + // Try to load User: + $userChanged = false; + $user = null; + if ($guid != "") { + $user = User::findOne(array('guid' => $guid, 'auth_mode' => User::AUTH_MODE_LDAP)); + } else { + // Fallback use e-mail + $user = User::findOne(array('email' => $email, 'auth_mode' => User::AUTH_MODE_LDAP)); + } + + if ($user === null) { + $user = new User(); + if ($guid != "") { + $user->guid = $guid; + } + $user->status = User::STATUS_ENABLED; + $user->auth_mode = User::AUTH_MODE_LDAP; + $user->group_id = 1; + + Yii::info('Create ldap user ' . $username . '!'); + } + + // Update Group Mapping + foreach (Group::find()->andWhere(['!=', 'ldap_dn', ""])->all() as $group) { + if (in_array($group->ldap_dn, $node->getAttribute('memberOf'))) { + if ($user->group_id != $group->id) { + $userChanged = true; + $user->group_id = $group->id; + } + } + } + + // Update Users Field + if ($user->username != $username) { + $userChanged = true; + $user->username = $username; + } + if ($user->email != $email) { + $userChanged = true; + $user->email = $email; + } + + if ($user->validate()) { + + // Only Save user when something is changed + if ($userChanged || $user->isNewRecord) + $user->save(); + + // Update Profile Fields + foreach (ProfileField::find()->andWhere(['!=', 'ldap_attribute', ''])->all() as $profileField) { + $ldapAttribute = $profileField->ldap_attribute; + $profileFieldName = $profileField->internal_name; + $user->profile->$profileFieldName = $node->getAttribute($ldapAttribute, 0); + } + + if ($user->profile->validate()) { + $user->profile->save(); + + // Update Space Mapping + foreach (Space::find()->andWhere(['!=', 'ldap_dn', ''])->all() as $space) { + if (in_array($space->ldap_dn, $node->getAttribute('memberOf'))) { + $space->addMember($user->id); + } + } + } else { + Yii::error('Could not create or update ldap user profile! (' . print_r($user->profile->getErrors(), true) . ")"); + } + } else { + Yii::error('Could not create or update ldap user! (' . print_r($user->getErrors(), true) . ")"); + } + + return $user; + } + + /** + * Converts LDAP Binary GUID to Ascii + * + * @param type $object_guid + * @return type + */ + private function binToStrGuid($object_guid) + { + $hex_guid = bin2hex($object_guid); + + if ($hex_guid == "") + return ""; + + $hex_guid_to_guid_str = ''; + for ($k = 1; $k <= 4; ++$k) { + $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2); + } + $hex_guid_to_guid_str .= '-'; + for ($k = 1; $k <= 2; ++$k) { + $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2); + } + $hex_guid_to_guid_str .= '-'; + for ($k = 1; $k <= 2; ++$k) { + $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2); + } + $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4); + $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20); + + return strtolower($hex_guid_to_guid_str); + } + + /** + * Checks if LDAP is supported + */ + public static function isAvailable() + { + if (!class_exists('Zend\Ldap\Ldap')) { + return false; + } + + if (!function_exists('ldap_bind')) { + return false; + } + + return true; + } + +} + +?> diff --git a/protected/humhub/modules/user/models/forms/AccountLogin.php b/protected/humhub/modules/user/models/forms/AccountLogin.php index 0b13ffaf87..09981c588d 100644 --- a/protected/humhub/modules/user/models/forms/AccountLogin.php +++ b/protected/humhub/modules/user/models/forms/AccountLogin.php @@ -5,6 +5,8 @@ namespace humhub\modules\user\models\forms; use Yii; use yii\base\Model; use humhub\modules\user\models\User; +use humhub\modules\user\libs\Ldap; +use humhub\models\Setting; /** * LoginForm is the model behind the login form. @@ -12,6 +14,9 @@ use humhub\modules\user\models\User; class AccountLogin extends Model { + /** + * @var string user's username or email address + */ public $username; public $password; public $rememberMe = true; @@ -41,9 +46,14 @@ class AccountLogin extends Model { if (!$this->hasErrors()) { $user = $this->getUser(); - if (!$user || !$user->currentPassword->validatePassword($this->password)) { - $this->addError($attribute, 'Incorrect username or password.'); + if ($user !== null) { + if ($user->auth_mode === User::AUTH_MODE_LOCAL && $user->currentPassword->validatePassword($this->password)) { + return; + } elseif ($user->auth_mode === User::AUTH_MODE_LDAP && Ldap::isAvailable() && Ldap::getInstance()->authenticate($user->username, $this->password)) { + return; + } } + $this->addError($attribute, 'Incorrect username or password.'); } } @@ -83,7 +93,18 @@ class AccountLogin extends Model public function getUser() { if ($this->_user === false) { - $this->_user = User::findOne(['username' => $this->username]); + $this->_user = User::find()->where(['username' => $this->username])->orWhere(['email' => $this->username])->one(); + + // Could not found user -> lookup in LDAP + if ($this->_user === null && Ldap::isAvailable() && Setting::Get('enabled', 'authentication_ldap')) { + + // Try load/create LDAP user + $usernameDn = Ldap::getInstance()->ldap->getCanonicalAccountName($this->username, \Zend\Ldap\Ldap::ACCTNAME_FORM_DN); + Ldap::getInstance()->handleLdapUser(Ldap::getInstance()->ldap->getNode($usernameDn)); + + // Check if user is availble now + $this->_user = User::find()->where(['username' => $this->username])->orWhere(['email' => $this->username])->one(); + } } return $this->_user;