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;