From 8191d4ef86e4490da1b0fa4d27f256de106f0f28 Mon Sep 17 00:00:00 2001 From: Lucas Bartholemy Date: Fri, 15 Jan 2016 01:53:10 +0100 Subject: [PATCH] Enh #117: Refactored authentication (pluggable yii2-authclient support) --- composer.json | 1 + protected/humhub/compat/HForm.php | 24 +- protected/humhub/config/common.php | 4 + protected/humhub/docs/guide/admin-adv-auth.md | 72 ++++ protected/humhub/libs/SelfTest.php | 2 +- protected/humhub/libs/StringHelper.php | 51 +++ .../admin/controllers/SettingController.php | 9 +- .../admin/controllers/UserController.php | 120 +----- .../admin/views/setting/authentication.php | 9 +- .../installer/commands/InstallController.php | 3 +- .../controllers/ConfigController.php | 6 +- protected/humhub/modules/user/Events.php | 23 +- protected/humhub/modules/user/Module.php | 22 ++ .../user/authclient/AuthClientHelpers.php | 238 ++++++++++++ .../modules/user/authclient/BaseClient.php | 32 ++ .../modules/user/authclient/BaseFormAuth.php | 37 ++ .../modules/user/authclient/Collection.php | 140 +++++++ .../modules/user/authclient/Password.php | 68 ++++ .../user/authclient/ZendLdapClient.php | 342 ++++++++++++++++++ .../authclient/interfaces/ApprovalBypass.php | 20 + .../authclient/interfaces/AutoSyncUsers.php | 22 ++ .../authclient/interfaces/PrimaryClient.php | 29 ++ .../authclient/interfaces/SyncAttributes.php | 29 ++ .../humhub/modules/user/components/User.php | 63 ++++ .../user/controllers/AccountController.php | 57 ++- .../user/controllers/AuthController.php | 161 +++++++-- .../controllers/RegistrationController.php | 208 +++++------ protected/humhub/modules/user/libs/Ldap.php | 281 -------------- .../migrations/m151226_164234_authclient.php | 37 ++ protected/humhub/modules/user/models/Auth.php | 59 +++ .../humhub/modules/user/models/Group.php | 21 ++ .../humhub/modules/user/models/Invite.php | 80 ++-- .../humhub/modules/user/models/Profile.php | 8 +- protected/humhub/modules/user/models/User.php | 53 +-- .../user/models/forms/AccountLogin.php | 120 ------ .../models/forms/AccountRecoverPassword.php | 4 +- .../user/models/forms/AccountRegister.php | 43 --- .../modules/user/models/forms/Login.php | 92 +++++ .../user/models/forms/Registration.php | 291 +++++++++++++++ .../user/views/account/connected-accounts.php | 35 ++ .../user/views/account/editSettings.php | 9 +- .../humhub/modules/user/views/auth/login.php | 15 +- .../modules/user/views/auth/login_modal.php | 17 +- .../modules/user/widgets/AccountMenu.php | 11 +- .../user/widgets/AccountSettingsMenu.php | 68 ++++ .../modules/user/widgets/AuthChoice.php | 83 +++++ .../tests/codeception/_pages/LoginPage.php | 4 +- 47 files changed, 2294 insertions(+), 829 deletions(-) create mode 100644 protected/humhub/docs/guide/admin-adv-auth.md create mode 100644 protected/humhub/libs/StringHelper.php create mode 100644 protected/humhub/modules/user/authclient/AuthClientHelpers.php create mode 100644 protected/humhub/modules/user/authclient/BaseClient.php create mode 100644 protected/humhub/modules/user/authclient/BaseFormAuth.php create mode 100644 protected/humhub/modules/user/authclient/Collection.php create mode 100644 protected/humhub/modules/user/authclient/Password.php create mode 100644 protected/humhub/modules/user/authclient/ZendLdapClient.php create mode 100644 protected/humhub/modules/user/authclient/interfaces/ApprovalBypass.php create mode 100644 protected/humhub/modules/user/authclient/interfaces/AutoSyncUsers.php create mode 100644 protected/humhub/modules/user/authclient/interfaces/PrimaryClient.php create mode 100644 protected/humhub/modules/user/authclient/interfaces/SyncAttributes.php delete mode 100644 protected/humhub/modules/user/libs/Ldap.php create mode 100644 protected/humhub/modules/user/migrations/m151226_164234_authclient.php create mode 100644 protected/humhub/modules/user/models/Auth.php delete mode 100644 protected/humhub/modules/user/models/forms/AccountLogin.php delete mode 100644 protected/humhub/modules/user/models/forms/AccountRegister.php create mode 100644 protected/humhub/modules/user/models/forms/Login.php create mode 100644 protected/humhub/modules/user/models/forms/Registration.php create mode 100644 protected/humhub/modules/user/views/account/connected-accounts.php create mode 100644 protected/humhub/modules/user/widgets/AccountSettingsMenu.php create mode 100644 protected/humhub/modules/user/widgets/AuthChoice.php diff --git a/composer.json b/composer.json index 024a3fc4f9..b9f9ffef2c 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "yiisoft/yii2": ">=2.0.4", "yiisoft/yii2-bootstrap": "*", "yiisoft/yii2-swiftmailer": "*", + "yiisoft/yii2-authclient": "*", "raoul2000/yii2-jcrop-widget": "*", "cebe/markdown": "1.0.2", "yiisoft/yii2-jui": "^2.0", diff --git a/protected/humhub/compat/HForm.php b/protected/humhub/compat/HForm.php index a31a3ad912..e04f37ee8a 100644 --- a/protected/humhub/compat/HForm.php +++ b/protected/humhub/compat/HForm.php @@ -24,10 +24,17 @@ class HForm extends \yii\base\Component public $models = array(); public $definition = array(); - public function __construct($definition, $primaryModel = null) + /** + * @var boolean manually mark form as submitted + */ + public $markedAsSubmitted = false; + + public function __construct($definition = [], $primaryModel = null) { $this->definition = $definition; $this->primaryModel = $primaryModel; + + $this->init(); } public function submitted($buttonName = "") @@ -43,6 +50,8 @@ class HForm extends \yii\base\Component } return true; } + } elseif ($this->markedAsSubmitted) { + return true; } return false; @@ -57,7 +66,7 @@ class HForm extends \yii\base\Component $hasErrors = true; } } - + foreach ($this->models as $model) { if (!$model->validate()) { $hasErrors = true; @@ -66,6 +75,17 @@ class HForm extends \yii\base\Component return !$hasErrors; } + public function clearErrors() + { + if ($this->primaryModel !== null) { + $this->primaryModel->clearErrors(); + } + + foreach ($this->models as $model) { + $model->clearErrors(); + } + } + public function save() { $hasErrors = false; diff --git a/protected/humhub/config/common.php b/protected/humhub/config/common.php index 10f4faeeba..d237d57261 100644 --- a/protected/humhub/config/common.php +++ b/protected/humhub/config/common.php @@ -85,6 +85,10 @@ $config = [ 'charset' => 'utf8', 'enableSchemaCache' => true, ], + 'authClientCollection' => [ + 'class' => 'humhub\modules\user\authclient\Collection', + 'clients' => [], + ], ], 'params' => [ 'installed' => false, diff --git a/protected/humhub/docs/guide/admin-adv-auth.md b/protected/humhub/docs/guide/admin-adv-auth.md new file mode 100644 index 0000000000..aaefe54790 --- /dev/null +++ b/protected/humhub/docs/guide/admin-adv-auth.md @@ -0,0 +1,72 @@ +Authentication +============== + +## Example: GitHub Authentication + +- Obtain Application ID and Secret from GitHub +- Add following block to protected/config/common.php + +``` + // ... + 'components' => [ + // ... + 'authClientCollection' => [ + 'class' => 'yii\authclient\Collection', + 'clients' => [ + 'github' => [ + 'class' => 'yii\authclient\clients\GitHub', + 'clientId' => '--->your-client-id<---', + 'clientSecret' => '--->your-client-secret<---', + 'normalizeUserAttributeMap' => [ + 'username' => 'login', + 'firstname' => function ($attributes) { + list($f, $l) = mb_split(' ', $attributes['name'], 2); + return $f; + }, + 'lastname' => function ($attributes) { + list($f, $l) = mb_split(' ', $attributes['name'], 2); + return $l; + }, + ], + ], + ], + ], + // .. + ], + // ... +``` + +## Example: Facebook + +- Obtain Application ID and Secret from Facebook +- Add following block to protected/config/common.php + +``` + // ... + 'components' => [ + // ... + 'authClientCollection' => [ + 'class' => 'yii\authclient\Collection', + 'clients' => [ + 'facebook' => [ + 'class' => 'yii\authclient\clients\Facebook', + 'clientId' => '1662633133992750', + 'clientSecret' => '3c786f625a26f7f3649bd5cc8d8c6a61', + 'normalizeUserAttributeMap' => [ + 'username' => 'name', + 'firstname' => function ($attributes) { + list($f, $l) = mb_split(' ', $attributes['name'], 2); + return $f; + }, + 'lastname' => function ($attributes) { + list($f, $l) = mb_split(' ', $attributes['name'], 2); + return $l; + }, + ], + ], + ], + ], + // .. + ], + // ... +``` diff --git a/protected/humhub/libs/SelfTest.php b/protected/humhub/libs/SelfTest.php index 3fc49e992f..a607cfdd7e 100644 --- a/protected/humhub/libs/SelfTest.php +++ b/protected/humhub/libs/SelfTest.php @@ -171,7 +171,7 @@ class SelfTest // Checks LDAP Extension $title = 'LDAP Support'; - if (\humhub\modules\user\libs\Ldap::isAvailable()) { + if (\humhub\modules\user\authclient\ZendLdapClient::isLdapAvailable()) { $checks[] = array( 'title' => Yii::t('base', $title), 'state' => 'OK' diff --git a/protected/humhub/libs/StringHelper.php b/protected/humhub/libs/StringHelper.php new file mode 100644 index 0000000000..a1a8fd59e5 --- /dev/null +++ b/protected/humhub/libs/StringHelper.php @@ -0,0 +1,51 @@ +response->redirect(Url::toRoute('/admin/setting/authentication-ldap')); } + $enabled = false; $userCount = 0; $errorMessage = ""; @@ -194,11 +195,9 @@ class SettingController extends Controller 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'); - } + $ldapAuthClient = new \humhub\modules\user\authclient\ZendLdapClient(); + $ldap = $ldapAuthClient->getLdap(); + $userCount = $ldap->count(Setting::Get('userFilter', 'authentication_ldap'), Setting::Get('baseDn', 'authentication_ldap'), \Zend\Ldap\Ldap::SEARCH_SCOPE_SUB); } catch (\Zend\Ldap\Exception\LdapException $ex) { $errorMessage = $ex->getMessage(); } catch (Exception $ex) { diff --git a/protected/humhub/modules/admin/controllers/UserController.php b/protected/humhub/modules/admin/controllers/UserController.php index bff13642df..a97e1e362a 100644 --- a/protected/humhub/modules/admin/controllers/UserController.php +++ b/protected/humhub/modules/admin/controllers/UserController.php @@ -11,9 +11,9 @@ namespace humhub\modules\admin\controllers; use Yii; use yii\helpers\Url; use humhub\compat\HForm; +use humhub\modules\user\models\forms\Registration; use humhub\modules\admin\components\Controller; use humhub\modules\user\models\User; -use humhub\modules\user\models\Password; use humhub\modules\user\models\Group; /** @@ -80,15 +80,6 @@ class UserController extends Controller 'super_admin' => array( 'type' => 'checkbox', ), - 'auth_mode' => array( - 'type' => 'dropdownlist', - 'disabled' => 'disabled', - 'class' => 'form-control', - 'items' => array( - User::AUTH_MODE_LOCAL => Yii::t('AdminModule.controllers_UserController', 'Local'), - User::AUTH_MODE_LDAP => Yii::t('AdminModule.controllers_UserController', 'LDAP'), - ), - ), 'status' => array( 'type' => 'dropdownlist', 'class' => 'form-control', @@ -149,110 +140,13 @@ class UserController extends Controller public function actionAdd() { - $userModel = new User(); - $userModel->scenario = 'registration'; - - $userPasswordModel = new Password(); - $userPasswordModel->scenario = 'registration'; - - $profileModel = $userModel->profile; - $profileModel->scenario = 'registration'; - - // Build Form Definition - $definition = array(); - $definition['elements'] = array(); - - $groupModels = \humhub\modules\user\models\Group::find()->orderBy('name ASC')->all(); - $defaultUserGroup = \humhub\models\Setting::Get('defaultUserGroup', 'authentication_internal'); - $groupFieldType = "dropdownlist"; - if ($defaultUserGroup != "") { - $groupFieldType = "hidden"; - } else if (count($groupModels) == 1) { - $groupFieldType = "hidden"; - $defaultUserGroup = $groupModels[0]->id; + $registration = new Registration(); + $registration->enableEmailField = true; + $registration->enableUserApproval = false; + if ($registration->submitted('save') && $registration->validate() && $registration->register()) { + return $this->redirect(Url::to(['edit', 'id' => $registration->getUser()->id])); } - if ($groupFieldType == 'hidden') { - $userModel->group_id = $defaultUserGroup; - } - // Add User Form - $definition['elements']['User'] = array( - 'type' => 'form', - 'title' => Yii::t('UserModule.controllers_AuthController', 'Account'), - 'elements' => array( - 'username' => array( - 'type' => 'text', - 'class' => 'form-control', - 'maxlength' => 25, - ), - 'email' => array( - 'type' => 'text', - 'class' => 'form-control', - 'maxlength' => 100, - ), - 'group_id' => array( - 'type' => $groupFieldType, - 'class' => 'form-control', - 'items' => \yii\helpers\ArrayHelper::map($groupModels, 'id', 'name'), - 'value' => $defaultUserGroup, - ), - ), - ); - - // Add User Password Form - $definition['elements']['UserPassword'] = array( - 'type' => 'form', - #'title' => 'Password', - 'elements' => array( - 'newPassword' => array( - 'type' => 'password', - 'class' => 'form-control', - 'maxlength' => 255, - ), - 'newPasswordConfirm' => array( - 'type' => 'password', - 'class' => 'form-control', - 'maxlength' => 255, - ), - ), - ); - - // Add Profile Form - $definition['elements']['Profile'] = array_merge(array('type' => 'form'), $profileModel->getFormDefinition()); - - // Get Form Definition - $definition['buttons'] = array( - 'save' => array( - 'type' => 'submit', - 'class' => 'btn btn-primary', - 'label' => Yii::t('UserModule.controllers_AuthController', 'Create account'), - ), - ); - - $form = new HForm($definition); - $form->models['User'] = $userModel; - $form->models['UserPassword'] = $userPasswordModel; - $form->models['Profile'] = $profileModel; - - if ($form->submitted('save') && $form->validate()) { - - $this->forcePostRequest(); - - $form->models['User']->status = User::STATUS_ENABLED; - if ($form->models['User']->save()) { - // Save User Profile - $form->models['Profile']->user_id = $form->models['User']->id; - $form->models['Profile']->save(); - - // Save User Password - $form->models['UserPassword']->user_id = $form->models['User']->id; - $form->models['UserPassword']->setPassword($form->models['UserPassword']->newPassword); - $form->models['UserPassword']->save(); - - return $this->redirect(Url::to(['index'])); - } - } - - return $this->render('add', array('hForm' => $form)); + return $this->render('add', array('hForm' => $registration)); } /** diff --git a/protected/humhub/modules/admin/views/setting/authentication.php b/protected/humhub/modules/admin/views/setting/authentication.php index dba276b5dd..d1fbf6d1ea 100644 --- a/protected/humhub/modules/admin/views/setting/authentication.php +++ b/protected/humhub/modules/admin/views/setting/authentication.php @@ -4,7 +4,6 @@ use yii\widgets\ActiveForm; use humhub\compat\CHtml; use humhub\models\Setting; use yii\helpers\Url; - ?>
Authentication - Basic'); ?>
@@ -14,11 +13,9 @@ use yii\helpers\Url;
  • - -
  • - -
  • - +
  • + +
  • diff --git a/protected/humhub/modules/installer/commands/InstallController.php b/protected/humhub/modules/installer/commands/InstallController.php index 2100b0abff..66bb2f9bae 100644 --- a/protected/humhub/modules/installer/commands/InstallController.php +++ b/protected/humhub/modules/installer/commands/InstallController.php @@ -18,7 +18,7 @@ use humhub\modules\space\models\Space; /** * Console Install - * + * * @author Luke */ class InstallController extends Controller @@ -38,7 +38,6 @@ class InstallController extends Controller $user = new User(); $user->group_id = 1; $user->username = "Admin"; - $user->auth_mode = 'local'; $user->email = 'humhub@example.com'; $user->status = User::STATUS_ENABLED; $user->language = ''; diff --git a/protected/humhub/modules/installer/controllers/ConfigController.php b/protected/humhub/modules/installer/controllers/ConfigController.php index 1af845f979..25d927c179 100644 --- a/protected/humhub/modules/installer/controllers/ConfigController.php +++ b/protected/humhub/modules/installer/controllers/ConfigController.php @@ -243,7 +243,6 @@ class ConfigController extends Controller $profileModel->scenario = 'registration'; $userModel->status = User::STATUS_ENABLED; - $userModel->auth_mode = "local"; $userModel->username = "david1986"; $userModel->email = "david.roberts@humhub.com"; $userModel->super_admin = 0; @@ -273,7 +272,6 @@ class ConfigController extends Controller $profileModel2->scenario = 'registration'; $userModel2->status = User::STATUS_ENABLED; - $userModel2->auth_mode = "local"; $userModel2->username = "sara1989"; $userModel2->email = "sara.schuster@humhub.com"; $userModel2->super_admin = 0; @@ -361,7 +359,7 @@ class ConfigController extends Controller $userModel = new User(); - $userModel->scenario = 'registration'; + $userModel->scenario = 'registration_email'; $userPasswordModel = new Password(); $userPasswordModel->scenario = 'registration'; $profileModel = $userModel->profile; @@ -482,7 +480,7 @@ class ConfigController extends Controller } \humhub\libs\DynamicConfig::rewrite(); - + $this->redirect(['finished']); } diff --git a/protected/humhub/modules/user/Events.php b/protected/humhub/modules/user/Events.php index de7679bc30..c30516218b 100644 --- a/protected/humhub/modules/user/Events.php +++ b/protected/humhub/modules/user/Events.php @@ -2,6 +2,7 @@ namespace humhub\modules\user; +use Yii; use humhub\modules\user\models\User; use humhub\modules\user\models\GroupAdmin; use humhub\modules\user\models\Password; @@ -22,7 +23,7 @@ class Events extends \yii\base\Object /** * On rebuild of the search index, rebuild all user records * - * @param type $event + * @param \yii\base\Event $event */ public static function onSearchRebuild($event) { @@ -34,7 +35,7 @@ class Events extends \yii\base\Object /** * On delete of a Content or ContentAddon * - * @param type $event + * @param \yii\base\Event $event */ public static function onContentDelete($event) { @@ -45,7 +46,7 @@ class Events extends \yii\base\Object /** * Callback to validate module database records. * - * @param Event $event + * @param \yii\base\Event $event */ public static function onIntegrityCheck($event) { @@ -126,14 +127,22 @@ class Events extends \yii\base\Object } } + /** + * Tasks on hourly cron job + * + * @param \yii\base\Event $event + */ 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); + foreach (Yii::$app->authClientCollection->getClients() as $authClient) { + if ($authClient instanceof authclient\interfaces\AutoSyncUsers) { + /** + * @var authclient\interfaces\AutoSyncUsers $authClient + */ + $authClient->syncUsers(); + } } } diff --git a/protected/humhub/modules/user/Module.php b/protected/humhub/modules/user/Module.php index c552b1e28d..d96396ed4b 100644 --- a/protected/humhub/modules/user/Module.php +++ b/protected/humhub/modules/user/Module.php @@ -1,10 +1,32 @@ getUserAttributes(); + + if ($authClient instanceof interfaces\PrimaryClient) { + /** + * @var interfaces\PrimaryClient $authClient + */ + return User::findOne([ + $authClient->getUserTableIdAttribute() => $attributes['id'], + 'auth_mode' => $authClient->getId() + ]); + } + + $auth = Auth::find()->where(['source' => $authClient->getId(), 'source_id' => $attributes['id']])->one(); + if ($auth !== null) { + return $auth->user; + } + } + + /** + * Stores an authClient to an user record + * + * @param \yii\authclient\BaseClient $authClient + * @param User $user + */ + public static function storeAuthClientForUser(ClientInterface $authClient, User $user) + { + $attributes = $authClient->getUserAttributes(); + + if ($authClient instanceof interfaces\PrimaryClient) { + $user->auth_mode = $authClient->getId(); + $user->save(); + } else { + $auth = Auth::findOne(['user_id' => $user->id, 'source' => $authClient->getId(), 'source_id' => $attributes['id']]); + + if ($auth === null) { + $auth = new \humhub\modules\user\models\Auth([ + 'user_id' => $user->id, + 'source' => (string) $authClient->getId(), + 'source_id' => (string) $attributes['id'], + ]); + + $auth->save(); + } + } + } + + /** + * Removes Authclient for a user + * + * @param \yii\authclient\BaseClient $authClient + * @param User $user + */ + public static function removeAuthClientForUser(ClientInterface $authClient, User $user) + { + Auth::deleteAll([ + 'user_id' => $user->id, + 'source' => (string) $authClient->getId() + ]); + } + + /** + * Updates (or creates) a user in HumHub using AuthClients Attributes + * This method will be called after login or by cron sync. + * + * @param \yii\authclient\BaseClient $authClient + * @param User $user + * @return boolean succeed + */ + public static function updateUser(ClientInterface $authClient, User $user = null) + { + if ($user === null) { + $user = self::getUserByAuthClient($authClient); + if ($user === null) { + return false; + } + } + + $authClient->trigger(BaseClient::EVENT_UPDATE_USER, new \yii\web\UserEvent(['identity' => $user])); + + if ($authClient instanceof interfaces\SyncAttributes) { + $attributes = $authClient->getUserAttributes(); + foreach ($authClient->getSyncAttributes() as $attributeName) { + if (isset($attributes[$attributeName])) { + if (in_array($attributeName, ['email', 'username'])) { + $user->setAttribute($attributeName, $attributes[$attributeName]); + } else { + $user->profile->setAttribute($attributeName, $attributes[$attributeName]); + } + } + } + + if (count($user->getDirtyAttributes()) !== 0 && !$user->save()) { + Yii::error('Could not update user attributes by AuthClient (UserId: ' . $user->id . ") - Error: " . print_r($user->getErrors(), 1)); + return false; + } + + if (count($user->profile->getDirtyAttributes()) !== 0 && !$user->profile->save()) { + Yii::error('Could not update user attributes by AuthClient (UserId: ' . $user->id . ") - Error: " . print_r($user->getErrors(), 1)); + return false; + } + } + + return true; + } + + /** + * Automatically creates user by auth client attributes + * + * @param \yii\authclient\BaseClient $authClient + * @return boolean success status + */ + public static function createUser(ClientInterface $authClient) + { + $attributes = $authClient->getUserAttributes(); + + if (!isset($attributes['id'])) { + return false; + } + + $registration = new \humhub\modules\user\models\forms\Registration(); + $registration->enablePasswordForm = false; + + if ($authClient instanceof interfaces\ApprovalBypass) { + $registration->enableUserApproval = false; + } + + unset($attributes['id']); + $registration->getUser()->setAttributes($attributes, false); + $registration->getProfile()->setAttributes($attributes, false); + + if ($registration->validate() && $registration->register($authClient)) { + return $registration->getUser(); + } + + return null; + } + + /** + * Returns all users which are using an given authclient + * + * @param ClientInterface $authClient + * @return \yii\db\ActiveQuery + */ + public static function getUsersByAuthClient(ClientInterface $authClient) + { + $query = User::find(); + + if ($authClient instanceof interfaces\PrimaryClient) { + $query->where([ + 'auth_mode' => $authClient->getId() + ]); + } else { + $query->where(['user_auth.source' => $authClient->getId()]); + } + + return $query; + } + + /** + * Returns AuthClients used by given User + * + * @param User $user + * @return ClientInterface[] the users authclients + */ + public static function getAuthClientsByUser(User $user) + { + $authClients = []; + + foreach (Yii::$app->authClientCollection->getClients() as $client) { + /** + * @var $client ClientInterface + */ + // Add primary authClient + if ($user->auth_mode == $client->getId()) { + $authClients[] = $client; + } + + // Add additional authClient + foreach ($user->auths as $auth) { + if ($auth->source == $client->getId()) { + $authClients[] = $client; + } + } + } + + return $authClients; + } + + /** + * Returns a list of all synchornized user attributes + * + * @param User $user + * @return array attribute names + */ + public static function getSyncAttributesByUser(User $user) + { + $attributes = []; + foreach (self::getAuthClientsByUser($user) as $authClient) { + if ($authClient instanceof interfaces\SyncAttributes) { + $attributes = array_merge($attributes, $authClient->getSyncAttributes()); + } + } + return $attributes; + } + +} diff --git a/protected/humhub/modules/user/authclient/BaseClient.php b/protected/humhub/modules/user/authclient/BaseClient.php new file mode 100644 index 0000000000..a861ee18c9 --- /dev/null +++ b/protected/humhub/modules/user/authclient/BaseClient.php @@ -0,0 +1,32 @@ + [...] + */ + private $_clients = []; + + /** + * @param array $clients list of auth clients + */ + public function setClients(array $clients) + { + $this->_clients = array_merge($this->getDefaultClients(), $clients); + $this->trigger(self::EVENT_AFTER_CLIENTS_SET); + } + + /** + * @return ClientInterface[] list of auth clients. + */ + public function getClients($load = true) + { + $clients = []; + foreach ($this->_clients as $id => $client) { + $clients[$id] = $this->getClient($id, $load); + } + + return $clients; + } + + /** + * @param string $id service id. + * @return ClientInterface auth client instance. + * @throws InvalidParamException on non existing client request. + */ + public function getClient($id, $load = true) + { + if (!array_key_exists($id, $this->_clients)) { + throw new InvalidParamException("Unknown auth client '{$id}'."); + } + if (!is_object($this->_clients[$id]) && $load) { + $this->_clients[$id] = $this->createClient($id, $this->_clients[$id]); + } + + return $this->_clients[$id]; + } + + /** + * Checks if client exists in the hub. + * @param string $id client id. + * @return boolean whether client exist. + */ + public function hasClient($id) + { + return array_key_exists($id, $this->_clients); + } + + /** + * Sets a client by id and config + * + * @param string $id auth client id. + * @param array $config auth client instance configuration. + */ + public function setClient($id, $config) + { + $this->_clients[$id] = $config; + } + + /** + * Removes client by id + * + * @param string $id client id. + */ + public function removeClient($id) + { + unset($this->_clients[$id]); + } + + /** + * Creates auth client instance from its array configuration. + * @param string $id auth client id. + * @param array $config auth client instance configuration. + * @return ClientInterface auth client instance. + */ + protected function createClient($id, $config) + { + $config['id'] = $id; + + return Yii::createObject($config); + } + + /** + * Returns the configuration of default auth clients + * + * @return array the default auth clients + */ + protected function getDefaultClients() + { + $clients = []; + + $clients['password'] = [ + 'class' => 'humhub\modules\user\authclient\Password' + ]; + + if (Setting::Get('enabled', 'authentication_ldap')) { + $clients['ldap'] = [ + 'class' => 'humhub\modules\user\authclient\ZendLdapClient' + ]; + } + + return $clients; + } + +} diff --git a/protected/humhub/modules/user/authclient/Password.php b/protected/humhub/modules/user/authclient/Password.php new file mode 100644 index 0000000000..df2cabb840 --- /dev/null +++ b/protected/humhub/modules/user/authclient/Password.php @@ -0,0 +1,68 @@ +where(['username' => $this->login->username])->orWhere(['email' => $this->login->password])->one(); + + if ($user !== null && $user->currentPassword !== null && $user->currentPassword->validatePassword($this->login->password)) { + $this->setUserAttributes(['id' => $user->id]); + return true; + } + + return false; + } + +} diff --git a/protected/humhub/modules/user/authclient/ZendLdapClient.php b/protected/humhub/modules/user/authclient/ZendLdapClient.php new file mode 100644 index 0000000000..1976954892 --- /dev/null +++ b/protected/humhub/modules/user/authclient/ZendLdapClient.php @@ -0,0 +1,342 @@ +idAttribute; + } + + /** + * @inheritdoc + */ + public function getUserTableIdAttribute() + { + return $this->userTableIdAttribute; + } + + /** + * @inheritdoc + */ + public function auth() + { + + $node = $this->getUserNode(); + if ($node !== null) { + $this->setUserAttributes($node->getAttributes()); + return true; + } + + return false; + } + + /** + * @inheritdoc + */ + protected function defaultNormalizeUserAttributeMap() + { + $map = []; + + // Username field + $usernameAttribute = Setting::Get('usernameAttribute', 'authentication_ldap'); + if ($usernameAttribute == '') { + $usernameAttribute = 'sAMAccountName'; + } + $map['username'] = strtolower($usernameAttribute); + + // E-Mail field + $emailAttribute = Setting::Get('emailAttribute', 'authentication_ldap'); + if ($emailAttribute == '') { + $emailAttribute = 'mail'; + } + $map['email'] = strtolower($emailAttribute); + + // Profile Field Mapping + foreach (ProfileField::find()->andWhere(['!=', 'ldap_attribute', ''])->all() as $profileField) { + $map[$profileField->internal_name] = strtolower($profileField->ldap_attribute); + } + + return $map; + } + + /** + * @inheritdoc + */ + protected function normalizeUserAttributes($attributes) + { + $normalized = []; + + // Fix LDAP Attributes + foreach ($attributes as $name => $value) { + if (is_array($value) && count($value) == 1) { + $normalized[$name] = $value[0]; + } else { + $normalized[$name] = $value; + } + } + + if (isset($normalized['objectguid'])) { + $normalized['objectguid'] = \humhub\libs\StringHelper::binaryToGuid($normalized['objectguid']); + } + + return parent::normalizeUserAttributes($normalized); + } + + /** + * @return array list of user attributes + */ + public function getUserAttributes() + { + $attributes = parent::getUserAttributes(); + + // Try to automatically set id and usertable id attribute by available attributes + if ($this->getIdAttribute() === null || $this->getUserTableIdAttribute() === null) { + if (isset($attributes['objectguid'])) { + $this->idAttribute = 'objectguid'; + $this->userTableIdAttribute = 'guid'; + } elseif (isset($attributes['mail'])) { + $this->idAttribute = 'mail'; + $this->userTableIdAttribute = 'email'; + } else { + throw new \yii\base\Exception("Could not automatically determine unique user id from ldap node!"); + } + } + + // Make sure id attributes sits on id attribute key + if (isset($attributes[$this->getIdAttribute()])) { + $attributes['id'] = $attributes[$this->getIdAttribute()]; + } + + // Map usertable id attribute against ldap id attribute + $attributes[$this->getUserTableIdAttribute()] = $attributes[$this->getIdAttribute()]; + + return $attributes; + } + + /** + * Returns Users LDAP Node + * + * @return Node the users ldap node + */ + protected function getUserNode() + { + $dn = $this->getUserDn(); + if ($dn !== '') { + return $this->getLdap()->getNode($dn); + } + + return null; + } + + /** + * Returns the users LDAP DN + * + * @return string the user dn if found + */ + protected function getUserDn() + { + // ToDo: Search user even by e-mail address + try { + $this->getLdap()->bind($this->login->username, $this->login->password); + return $this->getLdap()->getCanonicalAccountName($this->login->username, Ldap::ACCTNAME_FORM_DN); + } catch (LdapException $ex) { + // User not found in LDAP + } + return ''; + } + + /** + * Returns Zend LDAP + * + * @return \Zend\Ldap\Ldap + */ + public function getLdap() + { + if ($this->_ldap === null) { + $options = array( + 'host' => 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(); + } + + return $this->_ldap; + } + + /** + * Sets an Zend LDAP Instance + * + * @param \Zend\Ldap\Ldap $ldap + */ + public function setLdap(\Zend\Ldap\Ldap $ldap) + { + $this->_ldap = $ldap; + } + + /** + * @inheritdoc + */ + public function getSyncAttributes() + { + $attributes = ['username', 'email']; + + foreach (ProfileField::find()->andWhere(['!=', 'ldap_attribute', ''])->all() as $profileField) { + $attributes[] = $profileField->internal_name; + } + + return $attributes; + } + + /** + * Refresh ldap users + * + * New users (found in ldap) will be automatically created if all required fiĆ©lds are set. + * Profile fields which are bind to LDAP will automatically updated. + */ + public function syncUsers() + { + if (!Setting::Get('enabled', 'authentication_ldap') || !Setting::Get('refreshUsers', 'authentication_ldap')) { + return; + } + + $userFilter = Setting::Get('userFilter', 'authentication_ldap'); + $baseDn = Setting::Get('baseDn', 'authentication_ldap'); + $userCollection = $this->getLdap()->search($userFilter, $baseDn, Ldap::SEARCH_SCOPE_SUB); + + $ids = []; + foreach ($userCollection as $attributes) { + $authClient = new static; + $authClient->setUserAttributes($attributes); + $attributes = $authClient->getUserAttributes(); + + $user = AuthClientHelpers::getUserByAuthClient($authClient); + if ($user === null) { + if (!AuthClientHelpers::createUser($authClient)) { + Yii::warning('Could not automatically create LDAP user ' . $attributes['email'] . ' - check required attributes!'); + } + } else { + AuthClientHelpers::updateUser($authClient, $user); + } + + $ids[] = $attributes['id']; + } + + /** + * Since userTableAttribute can be automatically set on user attributes + * try to take it from initialized authclient instance. + */ + $userTableIdAttribute = $this->getUserTableIdAttribute(); + if ($authClient !== null) { + $userTableIdAttribute = $authClient->getUserTableIdAttribute(); + } + + // Disable not longer existing users + foreach (AuthClientHelpers::getUsersByAuthClient($this)->each() as $user) { + if ($user->status !== User::STATUS_DISABLED && !in_array($user->getAttribute($userTableIdAttribute), $ids)) { + $user->status = User::STATUS_DISABLED; + $user->save(); + Yii::warning('Disabled user ' . $user->username . ' (' . $user->id . ') - Not found in LDAP!'); + } + } + } + + /** + * Checks if LDAP is supported + */ + public static function isLdapAvailable() + { + if (!class_exists('Zend\Ldap\Ldap')) { + return false; + } + + if (!function_exists('ldap_bind')) { + return false; + } + + return true; + } + +} diff --git a/protected/humhub/modules/user/authclient/interfaces/ApprovalBypass.php b/protected/humhub/modules/user/authclient/interfaces/ApprovalBypass.php new file mode 100644 index 0000000000..2c96ca8a20 --- /dev/null +++ b/protected/humhub/modules/user/authclient/interfaces/ApprovalBypass.php @@ -0,0 +1,20 @@ +permissionManager; } + public function canChangePassword() + { + foreach ($this->getAuthClients() as $authClient) { + if ($authClient->className() == \humhub\modules\user\authclient\Password::className()) { + return true; + } + } + + return false; + } + + public function canChangeEmail() + { + if (in_array('email', AuthClientHelpers::getSyncAttributesByUser($this->getIdentity()))) { + return false; + } + + return true; + } + + public function canDeleteAccount() + { + foreach ($this->getAuthClients() as $authClient) { + if ($authClient instanceof \humhub\modules\user\authclient\interfaces\AutoSyncUsers) { + return false; + } + } + return true; + } + + public function getAuthClients() + { + if ($this->_authClients === null) { + $this->_authClients = AuthClientHelpers::getAuthClientsByUser($this->getIdentity()); + } + + return $this->_authClients; + } + + public function setCurrentAuthClient(ClientInterface $authClient) + { + Yii::$app->session->set('currentAuthClientId', $authClient->getId()); + } + + public function getCurrentAuthClient() + { + foreach ($this->getAuthClients() as $authClient) { + if ($authClient->getId() == Yii::$app->session->get('currentAuthClientId')) { + return $authClient; + } + } + return null; + } + } diff --git a/protected/humhub/modules/user/controllers/AccountController.php b/protected/humhub/modules/user/controllers/AccountController.php index d948db6e18..228194b8d8 100644 --- a/protected/humhub/modules/user/controllers/AccountController.php +++ b/protected/humhub/modules/user/controllers/AccountController.php @@ -109,6 +109,40 @@ class AccountController extends Controller return $this->render('editSettings', array('model' => $model, 'languages' => Yii::$app->params['availableLanguages'])); } + public function actionConnectedAccounts() + { + if (Yii::$app->request->isPost && Yii::$app->request->get('disconnect')) { + foreach (Yii::$app->user->getAuthClients() as $authClient) { + if ($authClient->getId() == Yii::$app->request->get('disconnect')) { + \humhub\modules\user\authclient\AuthClientHelpers::removeAuthClientForUser($authClient, Yii::$app->user->getIdentity()); + } + } + return $this->redirect(['connected-accounts']); + } + $clients = []; + foreach (Yii::$app->get('authClientCollection')->getClients() as $client) { + if (!$client instanceof humhub\modules\user\authclient\BaseFormAuth && !$client instanceof \humhub\modules\user\authclient\interfaces\PrimaryClient) { + $clients[] = $client; + } + } + + $currentAuthProviderId = ""; + if (Yii::$app->user->getCurrentAuthClient() !== null) { + $currentAuthProviderId = Yii::$app->user->getCurrentAuthClient()->getId(); + } + + $activeAuthClientIds = []; + foreach (Yii::$app->user->getAuthClients() as $authClient) { + $activeAuthClientIds[] = $authClient->getId(); + } + + return $this->render('connected-accounts', [ + 'authClients' => $clients, + 'currentAuthProviderId' => $currentAuthProviderId, + 'activeAuthClientIds' => $activeAuthClientIds + ]); + } + /** * Allows the user to enable user specifc modules */ @@ -159,8 +193,8 @@ class AccountController extends Controller $isSpaceOwner = false; $user = Yii::$app->user->getIdentity(); - if ($user->auth_mode != User::AUTH_MODE_LOCAL) { - throw new HttpException(500, 'This is not a local account! You cannot delete it. (e.g. LDAP)!'); + if (!Yii::$app->user->canDeleteAccount()) { + throw new HttpException(500, 'Account deletion not allowed'); } foreach (\humhub\modules\space\models\Membership::GetUserSpaces() as $space) { @@ -213,9 +247,8 @@ class AccountController extends Controller */ public function actionChangeEmail() { - $user = Yii::$app->user->getIdentity(); - if ($user->auth_mode != User::AUTH_MODE_LOCAL) { - throw new HttpException(500, Yii::t('UserModule.controllers_AccountController', 'You cannot change your e-mail address here.')); + if (!Yii::$app->user->canChangeEmail()) { + throw new HttpException(500, 'Change E-Mail is not allowed'); } $model = new \humhub\modules\user\models\forms\AccountChangeEmail; @@ -233,15 +266,15 @@ class AccountController extends Controller */ public function actionChangeEmailValidate() { - $user = Yii::$app->user->getIdentity(); - - if ($user->auth_mode != User::AUTH_MODE_LOCAL) { - throw new CHttpException(500, Yii::t('UserModule.controllers_AccountController', 'You cannot change your e-mail address here.')); + if (!Yii::$app->user->canChangeEmail()) { + throw new HttpException(500, 'Change E-Mail is not allowed'); } $token = Yii::$app->request->get('token'); $email = Yii::$app->request->get('email'); + $user = Yii::$app->user->getIdentity(); + // Check if Token is valid if (md5(\humhub\models\Setting::Get('secret') . $user->guid . $email) != $token) { throw new HttpException(404, Yii::t('UserModule.controllers_AccountController', 'Invalid link! Please make sure that you entered the entire url.')); @@ -264,10 +297,8 @@ class AccountController extends Controller */ public function actionChangePassword() { - $user = Yii::$app->user->getIdentity(); - - if ($user->auth_mode != User::AUTH_MODE_LOCAL) { - throw new CHttpException(500, Yii::t('UserModule.controllers_AccountController', 'You cannot change your e-mail address here.')); + if (!Yii::$app->user->canChangePassword()) { + throw new HttpException(500, 'Password change is not allowed'); } $userPassword = new \humhub\modules\user\models\Password(); diff --git a/protected/humhub/modules/user/controllers/AuthController.php b/protected/humhub/modules/user/controllers/AuthController.php index 8b367eb522..141785643e 100644 --- a/protected/humhub/modules/user/controllers/AuthController.php +++ b/protected/humhub/modules/user/controllers/AuthController.php @@ -10,6 +10,11 @@ namespace humhub\modules\user\controllers; use Yii; use humhub\components\Controller; +use humhub\modules\user\models\User; +use humhub\modules\user\models\Auth; +use humhub\modules\user\models\Invite; +use humhub\modules\user\models\forms\Login; +use humhub\modules\user\authclient\AuthClientHelpers; /** * AuthController handles login and logout @@ -38,60 +43,148 @@ class AuthController extends Controller 'captcha' => [ 'class' => 'yii\captcha\CaptchaAction', ], + 'external-auth' => [ + 'class' => 'yii\authclient\AuthAction', + 'successCallback' => [$this, 'onAuthSuccess'], + ], ]; } + /** + * @inheritdoc + */ + public function beforeAction($action) + { + // Remove authClient from session - if already exists + Yii::$app->session->remove('authClient'); + + return parent::beforeAction($action); + } + /** * Displays the login page */ public function actionLogin() { - // If user is already logged in, redirect him to the dashboard if (!Yii::$app->user->isGuest) { $this->redirect(Yii::$app->user->returnUrl); } - // Show/Allow Anonymous Registration - $loginModel = new \humhub\modules\user\models\forms\AccountLogin; - if ($loginModel->load(Yii::$app->request->post()) && $loginModel->login()) { - if (Yii::$app->request->getIsAjax()) { - return $this->htmlRedirect(Yii::$app->user->returnUrl); - } else { - return $this->redirect(Yii::$app->user->returnUrl); - } + // Login Form Handling + $login = new Login; + if ($login->load(Yii::$app->request->post()) && $login->validate()) { + return $this->onAuthSuccess($login->authClient); } - $loginModel->password = ""; - $canRegister = \humhub\models\Setting::Get('anonymousRegistration', 'authentication_internal'); - $registerModel = new \humhub\modules\user\models\forms\AccountRegister; - - if ($canRegister) { - if ($registerModel->load(Yii::$app->request->post()) && $registerModel->validate()) { - - $invite = \humhub\modules\user\models\Invite::findOne(['email' => $registerModel->email]); - if ($invite === null) { - $invite = new \humhub\modules\user\models\Invite(); - } - $invite->email = $registerModel->email; - $invite->source = \humhub\modules\user\models\Invite::SOURCE_SELF; - $invite->language = Yii::$app->language; - $invite->save(); - $invite->sendInviteMail(); - - if (Yii::$app->request->getIsAjax()) { - return $this->render('register_success_modal', ['model' => $registerModel]); - } else { - return $this->render('register_success', ['model' => $registerModel]); - } + // Self Invite + $invite = new Invite(); + $invite->scenario = 'invite'; + if ($invite->load(Yii::$app->request->post()) && $invite->selfInvite()) { + if (Yii::$app->request->getIsAjax()) { + return $this->render('register_success_modal', ['model' => $invite]); + } else { + return $this->render('register_success', ['model' => $invite]); } } if (Yii::$app->request->getIsAjax()) { - return $this->renderAjax('login_modal', array('model' => $loginModel, 'registerModel' => $registerModel, 'canRegister' => $canRegister)); - } else { - return $this->render('login', array('model' => $loginModel, 'registerModel' => $registerModel, 'canRegister' => $canRegister)); + return $this->renderAjax('login_modal', array('model' => $login, 'invite' => $invite, 'canRegister' => $invite->allowSelfInvite())); } + return $this->render('login', array('model' => $login, 'invite' => $invite, 'canRegister' => $invite->allowSelfInvite())); + } + + /** + * Handle successful authentication + * + * @param \yii\authclient\BaseClient $authClient + * @return Response + */ + public function onAuthSuccess(\yii\authclient\BaseClient $authClient) + { + $attributes = $authClient->getUserAttributes(); + + // User already logged in - Add new authclient to existing user + if (!Yii::$app->user->isGuest) { + AuthClientHelpers::storeAuthClientForUser($authClient, Yii::$app->user->getIdentity()); + return $this->redirect(['/user/account/connected-accounts']); + } + + // Login existing user + $user = AuthClientHelpers::getUserByAuthClient($authClient); + if ($user !== null) { + return $this->login($user, $authClient); + } + + // Check if E-Mail is given + if (!isset($attributes['email'])) { + Yii::$app->session->setFlash('error', "Missing E-Mail Attribute from AuthClient."); + return $this->redirect(['/user/auth/login']); + } + + if (!isset($attributes['id'])) { + Yii::$app->session->setFlash('error', "Missing ID AuthClient Attribute from AuthClient."); + return $this->redirect(['/user/auth/login']); + } + + // Check if e-mail is already taken + if (User::findOne(['email' => $attributes['email']]) !== null) { + Yii::$app->session->setFlash('error', Yii::t('UserModule.base', 'User with the same email already exists but isn\'t linked to you. Login using your email first to link it.')); + return $this->redirect(['/user/auth/login']); + } + + // Try automatically create user & login user + $user = AuthClientHelpers::createUser($authClient); + if ($user !== null) { + return $this->login($user, $authClient); + } + + // Make sure we normalized user attributes before put it in session (anonymous functions) + $authClient->setNormalizeUserAttributeMap([]); + + // Store authclient in session - for registration controller + Yii::$app->session->set('authClient', $authClient); + + // Start registration process + return $this->redirect(['/user/registration']); + } + + /** + * Login user + * + * @param User $user + * @param \yii\authclient\BaseClient $authClient + * @return Response the current response object + */ + protected function login($user, $authClient) + { + $redirectUrl = ['/user/auth/login']; + if ($user->status == User::STATUS_ENABLED) { + $duration = 0; + if ($authClient instanceof \humhub\modules\user\authclient\BaseFormAuth) { + if ($authClient->login->rememberMe) { + $duration = Yii::$app->getModule('user')->loginRememberMeDuration; + } + } + AuthClientHelpers::updateUser($authClient, $user); + + if (Yii::$app->user->login($user, $duration)) { + Yii::$app->user->setCurrentAuthClient($authClient); + $url = Yii::$app->user->returnUrl; + } + } elseif ($user->status == User::STATUS_DISABLED) { + Yii::$app->session->setFlash('error', 'Your account is disabled!'); + } elseif ($user->status == User::STATUS_NEED_APPROVAL) { + Yii::$app->session->setFlash('error', 'Your account is not approved yet!'); + } else { + Yii::$app->session->setFlash('error', 'Unknown user status!'); + } + + if (Yii::$app->request->getIsAjax()) { + return $this->htmlRedirect($redirectUrl); + } + + return $this->redirect($redirectUrl); } /** diff --git a/protected/humhub/modules/user/controllers/RegistrationController.php b/protected/humhub/modules/user/controllers/RegistrationController.php index d2767c059e..9a8b01080d 100644 --- a/protected/humhub/modules/user/controllers/RegistrationController.php +++ b/protected/humhub/modules/user/controllers/RegistrationController.php @@ -9,13 +9,15 @@ namespace humhub\modules\user\controllers; use Yii; -use yii\web\HttpException; use yii\helpers\Url; +use yii\web\HttpException; +use yii\authclient\ClientInterface; use humhub\components\Controller; -use humhub\modules\user\models\Invite; -use humhub\compat\HForm; use humhub\modules\user\models\User; -use humhub\modules\user\models\Password; +use humhub\modules\user\models\Invite; +use humhub\modules\user\models\forms\Registration; +use humhub\modules\user\authclient\AuthClientHelpers; +use humhub\modules\user\authclient\interfaces\ApprovalBypass; /** * RegistrationController handles new user registration @@ -35,6 +37,18 @@ class RegistrationController extends Controller */ public $subLayout = "_layout"; + /** + * @inheritdoc + */ + public function beforeAction($action) + { + if (!Yii::$app->user->isGuest) { + throw new HttpException(401, 'Your are already logged in! - Logout first!'); + } + + return parent::beforeAction($action); + } + /** * Registration Form * @@ -43,136 +57,78 @@ class RegistrationController extends Controller */ public function actionIndex() { - $needApproval = \humhub\models\Setting::Get('needApproval', 'authentication_internal'); + $registration = new Registration(); - if (!Yii::$app->user->isGuest) - throw new HttpException(401, 'Your are already logged in! - Logout first!'); + /** + * @var \yii\authclient\BaseClient + */ + $authClient = null; + $inviteToken = Yii::$app->request->get('token', ''); - - $userInvite = Invite::findOne(['token' => Yii::$app->request->get('token')]); - if (!$userInvite) - throw new HttpException(404, 'Token not found!'); - - if ($userInvite->language) - Yii::$app->language = $userInvite->language; - - $userModel = new User(); - $userModel->scenario = 'registration'; - $userModel->email = $userInvite->email; - - $userPasswordModel = new Password(); - $userPasswordModel->scenario = 'registration'; - - $profileModel = $userModel->profile; - $profileModel->scenario = 'registration'; - - // Build Form Definition - $definition = array(); - $definition['elements'] = array(); - - - $groupModels = \humhub\modules\user\models\Group::find()->orderBy('name ASC')->all(); - $defaultUserGroup = \humhub\models\Setting::Get('defaultUserGroup', 'authentication_internal'); - $groupFieldType = "dropdownlist"; - if ($defaultUserGroup != "") { - $groupFieldType = "hidden"; - } else if (count($groupModels) == 1) { - $groupFieldType = "hidden"; - $defaultUserGroup = $groupModels[0]->id; - } - if ($groupFieldType == 'hidden') { - $userModel->group_id = $defaultUserGroup; + if ($inviteToken != '') { + $this->handleInviteRegistration($inviteToken, $registration); + } elseif (Yii::$app->session->has('authClient')) { + $authClient = Yii::$app->session->get('authClient'); + $this->handleAuthClientRegistration($authClient, $registration); + } else { + Yii::$app->session->setFlash('error', 'Registration failed.'); + return $this->redirect(['/user/auth/login']); } - // Add User Form - $definition['elements']['User'] = array( - 'type' => 'form', - 'title' => Yii::t('UserModule.controllers_AuthController', 'Account'), - 'elements' => array( - 'username' => array( - 'type' => 'text', - 'class' => 'form-control', - 'maxlength' => 25, - ), - 'group_id' => array( - 'type' => $groupFieldType, - 'class' => 'form-control', - 'items' => \yii\helpers\ArrayHelper::map($groupModels, 'id', 'name'), - 'value' => $defaultUserGroup, - ), - ), - ); + if ($registration->submitted('save') && $registration->validate() && $registration->register($authClient)) { + Yii::$app->session->remove('authClient'); - // Add User Password Form - $definition['elements']['UserPassword'] = array( - 'type' => 'form', - #'title' => 'Password', - 'elements' => array( - 'newPassword' => array( - 'type' => 'password', - 'class' => 'form-control', - 'maxlength' => 255, - ), - 'newPasswordConfirm' => array( - 'type' => 'password', - 'class' => 'form-control', - 'maxlength' => 255, - ), - ), - ); - - // Add Profile Form - $definition['elements']['Profile'] = array_merge(array('type' => 'form'), $profileModel->getFormDefinition()); - - // Get Form Definition - $definition['buttons'] = array( - 'save' => array( - 'type' => 'submit', - 'class' => 'btn btn-primary', - 'label' => Yii::t('UserModule.controllers_AuthController', 'Create account'), - ), - ); - - $form = new HForm($definition); - $form->models['User'] = $userModel; - $form->models['UserPassword'] = $userPasswordModel; - $form->models['Profile'] = $profileModel; - - if ($form->submitted('save') && $form->validate()) { - - $this->forcePostRequest(); - - // Registe User - $form->models['User']->email = $userInvite->email; - $form->models['User']->language = Yii::$app->language; - if ($form->models['User']->save()) { - - // Save User Profile - $form->models['Profile']->user_id = $form->models['User']->id; - $form->models['Profile']->save(); - - // Save User Password - $form->models['UserPassword']->user_id = $form->models['User']->id; - $form->models['UserPassword']->setPassword($form->models['UserPassword']->newPassword); - $form->models['UserPassword']->save(); - - // Autologin user - if (!$needApproval) { - Yii::$app->user->switchIdentity($form->models['User']); - return $this->redirect(Url::to(['/dashboard/dashboard'])); - } - - return $this->render('success', array( - 'form' => $form, - 'needApproval' => $needApproval, - )); + // Autologin when user is enabled (no approval required) + if ($registration->getUser()->status === User::STATUS_ENABLED) { + Yii::$app->user->switchIdentity($registration->models['User']); + return $this->redirect(Url::to(['/dashboard/dashboard'])); } + + return $this->render('success', [ + 'form' => $registration, + 'needApproval' => ($registration->getUser()->status === User::STATUS_NEED_APPROVAL) + ]); } - return $this->render('index', array( - 'hForm' => $form, - 'needAproval' => $needApproval) - ); + return $this->render('index', ['hForm' => $registration]); + } + + protected function handleInviteRegistration($inviteToken, Registration $form) + { + $userInvite = Invite::findOne(['token' => $inviteToken]); + if (!$userInvite) { + throw new HttpException(404, 'Invalid registration token!'); + } + if ($userInvite->language) { + Yii::$app->language = $userInvite->language; + } + $form->getUser()->email = $userInvite->email; + } + + /** + * @param \yii\authclient\BaseClient $authClient + * @param Registration $registration + * @return boolean already all registration data gathered + * @throws Exception + */ + protected function handleAuthClientRegistration(ClientInterface $authClient, Registration $registration) + { + $attributes = $authClient->getUserAttributes(); + + if (!isset($attributes['id'])) { + throw new Exception("No user id given by authclient!"); + } + + $registration->enablePasswordForm = false; + if ($authClient instanceof ApprovalBypass) { + $registration->enableUserApproval = false; + } + + // do not store id attribute + unset($attributes['id']); + + $registration->getUser()->setAttributes($attributes, false); + $registration->getProfile()->setAttributes($attributes, false); } } diff --git a/protected/humhub/modules/user/libs/Ldap.php b/protected/humhub/modules/user/libs/Ldap.php deleted file mode 100644 index 4e4ecde48d..0000000000 --- a/protected/humhub/modules/user/libs/Ldap.php +++ /dev/null @@ -1,281 +0,0 @@ - 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) - { - try { - $username = $this->ldap->getCanonicalAccountName($username, \Zend\Ldap\Ldap::ACCTNAME_FORM_DN); - $this->ldap->bind($username, $password); - - // Update Users Data - $node = $this->ldap->getNode($username); - $this->handleLdapUser($node); - - return true; - } catch (\Zend\Ldap\Exception\LdapException $ex) { - return false; - } catch (Exception $ex) { - 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) - { - - $usernameAttribute = Setting::Get('usernameAttribute', 'authentication_ldap'); - if ($usernameAttribute == '') { - $usernameAttribute = 'sAMAccountName'; - } - - $emailAttribute = Setting::Get('emailAttribute', 'authentication_ldap'); - if ($emailAttribute == '') { - $emailAttribute = 'mail'; - } - - $username = $node->getAttribute($usernameAttribute, 0); - $email = $node->getAttribute($emailAttribute, 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 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()) { - $this->trigger(self::EVENT_UPDATE_USER, new ParameterEvent(['user' => $user, 'node' => $node])); - } 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 '.$user->username.'! (' . 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/migrations/m151226_164234_authclient.php b/protected/humhub/modules/user/migrations/m151226_164234_authclient.php new file mode 100644 index 0000000000..3f5d91f4c5 --- /dev/null +++ b/protected/humhub/modules/user/migrations/m151226_164234_authclient.php @@ -0,0 +1,37 @@ +createTable('user_auth', array( + 'id' => $this->primaryKey(), + 'user_id' => $this->integer()->notNull(), + 'source' => $this->string(255)->notNull(), + 'source_id' => $this->string(255)->notNull(), + )); + $this->addForeignKey('fk_user_id', 'user_auth', 'user_id', 'user', 'id', 'CASCADE', 'CASCADE'); + } + + public function down() + { + echo "m151226_164234_authclient cannot be reverted.\n"; + + return false; + } + + /* + // Use safeUp/safeDown to run migration code within a transaction + public function safeUp() + { + } + + public function safeDown() + { + } + */ +} diff --git a/protected/humhub/modules/user/models/Auth.php b/protected/humhub/modules/user/models/Auth.php new file mode 100644 index 0000000000..4f6d7088d2 --- /dev/null +++ b/protected/humhub/modules/user/models/Auth.php @@ -0,0 +1,59 @@ + 255] + ]; + } + + /** + * @inheritdoc + */ + public function attributeLabels() + { + return [ + 'id' => 'ID', + 'user_id' => 'User ID', + 'source' => 'Source', + 'source_id' => 'Source ID', + ]; + } + + /** + * @return \yii\db\ActiveQuery + */ + public function getUser() + { + return $this->hasOne(User::className(), ['id' => 'user_id']); + } +} diff --git a/protected/humhub/modules/user/models/Group.php b/protected/humhub/modules/user/models/Group.php index a4ecc7b1d2..8a2a4ad18c 100644 --- a/protected/humhub/modules/user/models/Group.php +++ b/protected/humhub/modules/user/models/Group.php @@ -187,4 +187,25 @@ class Group extends \yii\db\ActiveRecord return true; } + /** + * Returns groups which are available in user registration + */ + public static function getRegistrationGroups() + { + $groups = []; + + $defaultGroup = \humhub\models\Setting::Get('defaultUserGroup', 'authentication_internal'); + if ($defaultGroup != '') { + $group = self::findOne(['id' => $defaultGroup]); + if ($group !== null) { + $groups[] = $group; + return $groups; + } + } else { + $groups = self::find()->orderBy('name ASC')->all(); + } + + return $groups; + } + } diff --git a/protected/humhub/modules/user/models/Invite.php b/protected/humhub/modules/user/models/Invite.php index 55c5ba3434..4a95c6dc0d 100644 --- a/protected/humhub/modules/user/models/Invite.php +++ b/protected/humhub/modules/user/models/Invite.php @@ -32,6 +32,7 @@ class Invite extends \yii\db\ActiveRecord const SOURCE_SELF = "self"; const SOURCE_INVITE = "invite"; + const TOKEN_LENGTH = 12; /** * @inheritdoc @@ -48,50 +49,61 @@ class Invite extends \yii\db\ActiveRecord { return [ [['user_originator_id', 'space_invite_id', 'created_by', 'updated_by'], 'integer'], - [['email'], 'required'], - [['created_at', 'updated_at', 'firstname', 'lastname'], 'safe'], - [['email', 'source', 'token'], 'string', 'max' => 45], - [['language'], 'string', 'max' => 10], - [['email'], 'unique'], [['token'], 'unique'], [['firstname', 'lastname'], 'string', 'max' => 255], + [['email', 'source', 'token'], 'string', 'max' => 45], + [['language'], 'string', 'max' => 10], + [['email'], 'required'], + [['email'], 'unique'], + [['email'], 'email'], + [['email'], 'unique', 'targetClass' => \humhub\modules\user\models\User::className(), 'message' => Yii::t('UserModule.base', 'E-Mail is already in use! - Try forgot password.')], ]; } /** * @inheritdoc */ - public function attributeLabels() + public function scenarios() { - return [ - 'id' => 'ID', - 'user_originator_id' => 'User Originator ID', - 'space_invite_id' => 'Space Invite ID', - 'email' => 'Email', - 'source' => 'Source', - 'token' => 'Token', - 'created_at' => 'Created At', - 'created_by' => 'Created By', - 'updated_at' => 'Updated At', - 'updated_by' => 'Updated By', - 'language' => 'Language', - 'firstname' => 'Firstname', - 'lastname' => 'Lastname' - ]; + $scenarios = parent::scenarios(); + $scenarios['invite'] = ['email']; + return $scenarios; } + /** + * @inheritdoc + */ public function beforeSave($insert) { - if ($insert) { - $this->token = uniqid() . mt_rand(); + if ($insert && $this->token == '') { + $this->token = Yii::$app->security->generateRandomString(self::TOKEN_LENGTH); } return parent::beforeSave($insert); } + public function selfInvite() + { + $this->source = self::SOURCE_SELF; + $this->language = Yii::$app->language; + + // Delete existing invite for e-mail - but reuse token + $existingInvite = Invite::findOne(['email' => $this->email]); + if ($existingInvite !== null) { + $this->token = $existingInvite->token; + $existingInvite->delete(); + } + + if ($this->allowSelfInvite() && $this->validate() && $this->save()) { + $this->sendInviteMail(); + return true; + } + + return false; + } + /** * Sends the invite e-mail - * */ public function sendInviteMail() { @@ -128,14 +140,34 @@ class Invite extends \yii\db\ActiveRecord } } + /** + * Return user which triggered this invite + * + * @return \yii\db\ActiveQuery + */ public function getOriginator() { return $this->hasOne(\humhub\modules\user\models\User::className(), ['id' => 'user_originator_id']); } + /** + * Return space which is involved in this invite + * + * @return \yii\db\ActiveQuery + */ public function getSpace() { return $this->hasOne(\humhub\modules\space\models\Space::className(), ['id' => 'space_invite_id']); } + /** + * Allow users to invite themself + * + * @return boolean allow self invite + */ + public function allowSelfInvite() + { + return (\humhub\models\Setting::Get('anonymousRegistration', 'authentication_internal')); + } + } diff --git a/protected/humhub/modules/user/models/Profile.php b/protected/humhub/modules/user/models/Profile.php index 19a33a2a3d..d5932b80e3 100644 --- a/protected/humhub/modules/user/models/Profile.php +++ b/protected/humhub/modules/user/models/Profile.php @@ -160,10 +160,14 @@ class Profile extends \yii\db\ActiveRecord */ public function getFormDefinition() { - $definition = array(); $definition['elements'] = array(); + $syncAttributes = []; + if ($this->user !== null) { + $syncAttributes = \humhub\modules\user\authclient\AuthClientHelpers::getSyncAttributesByUser($this->user); + } + foreach (ProfileFieldCategory::find()->orderBy('sort_order')->all() as $profileFieldCategory) { $category = array( @@ -190,7 +194,7 @@ class Profile extends \yii\db\ActiveRecord } // Dont allow editing of ldap syned fields - will be overwritten on next ldap sync. - if ($this->user !== null && $this->user->auth_mode == User::AUTH_MODE_LDAP && $profileField->ldap_attribute != "") { + if (in_array($profileField->internal_name, $syncAttributes)) { $profileField->editable = false; } diff --git a/protected/humhub/modules/user/models/User.php b/protected/humhub/modules/user/models/User.php index 8a8afa915c..627feede58 100644 --- a/protected/humhub/modules/user/models/User.php +++ b/protected/humhub/modules/user/models/User.php @@ -41,12 +41,6 @@ use humhub\modules\user\components\ActiveQueryUser; class User extends ContentContainerActiveRecord implements \yii\web\IdentityInterface, \humhub\modules\search\interfaces\Searchable { - /** - * Authentication Modes - */ - const AUTH_MODE_LDAP = "ldap"; - const AUTH_MODE_LOCAL = "local"; - /** * User Status Flags */ @@ -90,10 +84,11 @@ class User extends ContentContainerActiveRecord implements \yii\web\IdentityInte [['guid'], 'string', 'max' => 45], [['username'], 'string', 'max' => 25, 'min' => Yii::$app->params['user']['minUsernameLength']], [['time_zone'], 'in', 'range' => \DateTimeZone::listIdentifiers()], - [['email'], 'string', 'max' => 100], [['auth_mode'], 'string', 'max' => 10], [['language'], 'string', 'max' => 5], [['email'], 'unique'], + [['email'], 'email'], + [['email'], 'string', 'max' => 100], [['username'], 'unique'], [['guid'], 'unique'], [['wall_id'], 'unique'] @@ -122,8 +117,9 @@ class User extends ContentContainerActiveRecord implements \yii\web\IdentityInte { $scenarios = parent::scenarios(); $scenarios['login'] = ['username', 'password']; - $scenarios['editAdmin'] = ['username', 'email', 'group_id', 'super_admin', 'auth_mode', 'status']; - $scenarios['registration'] = ['username', 'email', 'group_id']; + $scenarios['editAdmin'] = ['username', 'email', 'group_id', 'super_admin', 'status']; + $scenarios['registration_email'] = ['username', 'email', 'group_id']; + $scenarios['registration'] = ['username', 'group_id']; return $scenarios; } @@ -176,7 +172,7 @@ class User extends ContentContainerActiveRecord implements \yii\web\IdentityInte /** * @inheritdoc - * + * * @return ActiveQueryContent */ public static function find() @@ -261,11 +257,11 @@ class User extends ContentContainerActiveRecord implements \yii\web\IdentityInte */ public function beforeSave($insert) { - if ($insert) { - if ($this->auth_mode == "") { - $this->auth_mode = self::AUTH_MODE_LOCAL; + if ($this->auth_mode == '') { + $passwordAuth = new \humhub\modules\user\authclient\Password(); + $this->auth_mode = $passwordAuth->getId(); } if (\humhub\models\Setting::Get('allowGuestAccess', 'authentication_internal')) { @@ -277,13 +273,8 @@ class User extends ContentContainerActiveRecord implements \yii\web\IdentityInte $this->last_activity_email = new \yii\db\Expression('NOW()'); - // Set Status if ($this->status == "") { - if (\humhub\models\Setting::Get('needApproval', 'authentication_internal')) { - $this->status = User::STATUS_NEED_APPROVAL; - } else { - $this->status = User::STATUS_ENABLED; - } + $this->status = self::STATUS_ENABLED; } if ((\humhub\models\Setting::Get('defaultUserGroup', 'authentication_internal'))) { @@ -296,7 +287,17 @@ class User extends ContentContainerActiveRecord implements \yii\web\IdentityInte } if ($this->group_id == "") { - throw new \yii\base\Exception("Could not save user without group!"); + // Try autoset group + $availableGroups = Group::getRegistrationGroups(); + $defaultUserGroup = \humhub\models\Setting::Get('defaultUserGroup', 'authentication_internal'); + if ($defaultUserGroup != '') { + $this->group_id = $defaultUserGroup; + } elseif (isset($availableGroups[0])) { + // Fallback to first available group + $this->group_id = $availableGroups[0]->id; + } else { + throw new \yii\base\Exception("Could not save user without group!"); + } } return parent::beforeSave($insert); @@ -409,7 +410,7 @@ class User extends ContentContainerActiveRecord implements \yii\web\IdentityInte /** * Checks if user has tags - * + * * @return boolean has tags set */ public function hasTags() @@ -419,7 +420,7 @@ class User extends ContentContainerActiveRecord implements \yii\web\IdentityInte /** * Returns an array with assigned Tags - * + * * @return array tags */ public function getTags() @@ -525,4 +526,12 @@ class User extends ContentContainerActiveRecord implements \yii\web\IdentityInte return false; } + /** + * @return type + */ + public function getAuths() + { + return $this->hasMany(\humhub\modules\user\models\Auth::className(), ['user_id' => 'id']); + } + } diff --git a/protected/humhub/modules/user/models/forms/AccountLogin.php b/protected/humhub/modules/user/models/forms/AccountLogin.php deleted file mode 100644 index 2d7c2e5de3..0000000000 --- a/protected/humhub/modules/user/models/forms/AccountLogin.php +++ /dev/null @@ -1,120 +0,0 @@ -hasErrors()) { - $user = $this->getUser(); - 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.'); - } - } - - public function validateUser($attribute, $params) - { - if (!$this->hasErrors()) { - $user = $this->getUser(); - if ($user !== null) { - if ($user->status == User::STATUS_DISABLED) { - $this->addError($attribute, 'Your account is disabled!'); - } - if ($user->status == User::STATUS_NEED_APPROVAL) { - $this->addError($attribute, 'Your account is not approved yet!'); - } - } - } - } - - /** - * Logs in a user using the provided username and password. - * @return boolean whether the user is logged in successfully - */ - public function login() - { - if ($this->validate() && Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0)) { - $this->_user->last_login = new Expression('NOW()'); - $this->_user->save(); - return true; - } else { - return false; - } - } - - /** - * Finds user by [[username]] - * - * @return User|null - */ - public function getUser() - { - if ($this->_user === false) { - $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 { - // 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(); - } catch (\Zend\Ldap\Exception\LdapException $ex) { - // User not found - } - } - } - - return $this->_user; - } - -} diff --git a/protected/humhub/modules/user/models/forms/AccountRecoverPassword.php b/protected/humhub/modules/user/models/forms/AccountRecoverPassword.php index 0d6b599efb..1c7f57ebc3 100644 --- a/protected/humhub/modules/user/models/forms/AccountRecoverPassword.php +++ b/protected/humhub/modules/user/models/forms/AccountRecoverPassword.php @@ -52,7 +52,9 @@ class AccountRecoverPassword extends \yii\base\Model if ($this->email != "") { $user = User::findOne(array('email' => $this->email)); - if ($user != null && $user->auth_mode != "local") { + $passwordAuth = new \humhub\modules\user\authclient\Password(); + + if ($user != null && $user->auth_mode !== $passwordAuth->getId()) { $this->addError($attribute, Yii::t('UserModule.forms_AccountRecoverPasswordForm', Yii::t('UserModule.forms_AccountRecoverPasswordForm', "Password recovery is not possible on your account type!"))); } } diff --git a/protected/humhub/modules/user/models/forms/AccountRegister.php b/protected/humhub/modules/user/models/forms/AccountRegister.php deleted file mode 100644 index fcf0140b02..0000000000 --- a/protected/humhub/modules/user/models/forms/AccountRegister.php +++ /dev/null @@ -1,43 +0,0 @@ - \humhub\modules\user\models\User::className(), 'message' => Yii::t('UserModule.forms_AccountRegisterForm', 'E-Mail is already in use! - Try forgot password.')], - ); - } - - /** - * Declares customized attribute labels. - * If not declared here, an attribute would have a label that is - * the same as its name with the first letter in upper case. - */ - public function attributeLabels() - { - return array( - 'email' => Yii::t('UserModule.forms_AccountRegisterForm', 'E-Mail'), - ); - } - -} diff --git a/protected/humhub/modules/user/models/forms/Login.php b/protected/humhub/modules/user/models/forms/Login.php new file mode 100644 index 0000000000..8c06b941e4 --- /dev/null +++ b/protected/humhub/modules/user/models/forms/Login.php @@ -0,0 +1,92 @@ +rememberMe = Yii::$app->getModule('user')->loginRememberMeDefault; + + parent::init(); + } + + /** + * Validation + */ + public function afterValidate() + { + $user = null; + + // Loop over enabled authclients + foreach (Yii::$app->authClientCollection->getClients() as $authClient) { + if ($authClient instanceof BaseFormAuth) { + $authClient->login = $this; + if ($authClient->auth()) { + $this->authClient = $authClient; + + // Delete password after successful auth + $this->password = ""; + + return; + } + } + } + + if ($user === null) { + $this->addError('password', 'User or Password incorrect.'); + } + + // Delete current password value + $this->password = ""; + + parent::afterValidate(); + } + +} diff --git a/protected/humhub/modules/user/models/forms/Registration.php b/protected/humhub/modules/user/models/forms/Registration.php new file mode 100644 index 0000000000..5deae145c2 --- /dev/null +++ b/protected/humhub/modules/user/models/forms/Registration.php @@ -0,0 +1,291 @@ +enableUserApproval = true; + } else { + $this->enableUserApproval = false; + } + + return parent::init(); + } + + /** + * @inheritdoc + */ + public function render($form) + { + $this->setFormDefinition(); + return parent::render($form); + } + + /** + * Builds HForm Definition to automatically build form output + */ + protected function setFormDefinition() + { + $this->definition = []; + $this->definition['elements'] = []; + $this->definition['elements']['User'] = $this->getUserFormDefinition(); + if ($this->enablePasswordForm) { + $this->definition['elements']['Password'] = $this->getPasswordFormDefinition(); + } + $this->definition['elements']['Profile'] = array_merge(array('type' => 'form'), $this->getProfile()->getFormDefinition()); + $this->definition['buttons'] = array( + 'save' => array( + 'type' => 'submit', + 'class' => 'btn btn-primary', + 'label' => Yii::t('UserModule.controllers_AuthController', 'Create account'), + ), + ); + } + + /** + * Create User Model form fields required for registration + * + * @return array form definition + */ + protected function getUserFormDefinition() + { + $groupModels = \humhub\modules\user\models\Group::find()->orderBy('name ASC')->all(); + $defaultUserGroup = \humhub\models\Setting::Get('defaultUserGroup', 'authentication_internal'); + $groupFieldType = "dropdownlist"; + if ($defaultUserGroup != "") { + $groupFieldType = "hidden"; + } else if (count($groupModels) == 1) { + $groupFieldType = "hidden"; + $defaultUserGroup = $groupModels[0]->id; + } + if ($groupFieldType == 'hidden') { + $this->getUser()->group_id = $defaultUserGroup; + } + + $form = array( + 'type' => 'form', + 'title' => Yii::t('UserModule.controllers_AuthController', 'Account'), + 'elements' => [], + ); + + $form['elements']['username'] = [ + 'type' => 'text', + 'class' => 'form-control', + 'maxlength' => 25, + ]; + if ($this->enableEmailField) { + $form['elements']['email'] = [ + 'type' => 'text', + 'class' => 'form-control', + ]; + } + $form['elements']['group_id'] = [ + 'type' => $groupFieldType, + 'class' => 'form-control', + 'items' => ArrayHelper::map($groupModels, 'id', 'name'), + 'value' => $defaultUserGroup, + ]; + + return $form; + } + + /** + * Create Password Model form fields required for registration + * + * @return array form definition + */ + protected function getPasswordFormDefinition() + { + return array( + 'type' => 'form', + 'elements' => array( + 'newPassword' => array( + 'type' => 'password', + 'class' => 'form-control', + 'maxlength' => 255, + ), + 'newPasswordConfirm' => array( + 'type' => 'password', + 'class' => 'form-control', + 'maxlength' => 255, + ), + ), + ); + } + + /** + * Set models User, Profile and Password to Form + */ + protected function setModels() + { + // Set Models + $this->models['User'] = $this->getUser(); + $this->models['Profile'] = $this->getProfile(); + if ($this->enablePasswordForm) { + $this->models['Password'] = $this->getPassword(); + } + + return true; + } + + /** + * @inheritdoc + */ + public function validate() + { + + // Ensure Models + $this->setModels(); + + return parent::validate(); + } + + /** + * @inheritdoc + */ + public function submitted($buttonName = "") + { + // Ensure Models + $this->setModels(); + + return parent::submitted($buttonName); + } + + /** + * Registers users + * + * @return boolean state + */ + public function register(\yii\authclient\ClientInterface $authClient = null) + { + $this->models['User']->language = Yii::$app->language; + if ($this->enableUserApproval) { + $this->models['User']->status = User::STATUS_NEED_APPROVAL; + } + + if ($this->models['User']->save()) { + + // Save User Profile + $this->models['Profile']->user_id = $this->models['User']->id; + $this->models['Profile']->save(); + + if ($this->enablePasswordForm) { + // Save User Password + $this->models['Password']->user_id = $this->models['User']->id; + $this->models['Password']->setPassword($this->models['Password']->newPassword); + $this->models['Password']->save(); + } + + if ($authClient !== null) { + \humhub\modules\user\authclient\AuthClientHelpers::storeAuthClientForUser($authClient, $this->models['User']); + $authClient->trigger(\humhub\modules\user\authclient\BaseClient::EVENT_CREATE_USER, new \yii\web\UserEvent(['identity' => $this->models['User']])); + } + + return true; + } + + return false; + } + + /** + * Returns User model + * + * @return User + */ + public function getUser() + { + if ($this->_user === null) { + $this->_user = new User(); + if ($this->enableEmailField) { + $this->_user->scenario = 'registration_email'; + } else { + $this->_user->scenario = 'registration'; + } + } + + return $this->_user; + } + + /** + * Returns Profile model + * + * @return Profile + */ + public function getProfile() + { + if ($this->_profile === null) { + $this->_profile = $this->getUser()->profile; + $this->_profile->scenario = 'registration'; + } + + return $this->_profile; + } + + /** + * Returns Password model + * + * @return Password + */ + public function getPassword() + { + if ($this->_password === null) { + $this->_password = new Password(); + $this->_password->scenario = 'registration'; + } + + return $this->_password; + } + +} diff --git a/protected/humhub/modules/user/views/account/connected-accounts.php b/protected/humhub/modules/user/views/account/connected-accounts.php new file mode 100644 index 0000000000..371bf6abaf --- /dev/null +++ b/protected/humhub/modules/user/views/account/connected-accounts.php @@ -0,0 +1,35 @@ + + +
    + Connected accounts'); ?> +
    +
    + +
    +

    + + +

    +
    +

    getTitle(); ?>

    + getId() == $currentAuthProviderId): ?> + 'btn btn-default pull-right', 'data-method' => 'POST', 'disabled' => 'disabled']); ?> + getId(), $activeAuthClientIds)) : ?> + $client->getId()], ['class' => 'btn btn-danger pull-right', 'data-method' => 'POST']); ?> + + $client->getId()]), ['class' => 'btn btn-success pull-right']); ?> + + +
    +
    + + + + +
    diff --git a/protected/humhub/modules/user/views/account/editSettings.php b/protected/humhub/modules/user/views/account/editSettings.php index 085f1d9e0b..54fa605217 100644 --- a/protected/humhub/modules/user/views/account/editSettings.php +++ b/protected/humhub/modules/user/views/account/editSettings.php @@ -9,6 +9,9 @@ use \humhub\models\Setting; User settings'); ?>
    + +
    +

    'basic-settings-form']); ?> field($model, 'tags'); ?> @@ -19,10 +22,12 @@ use \humhub\models\Setting; - field($model, 'visibility')->dropdownList([ + field($model, 'visibility')->dropdownList([ 1 => Yii::t('UserModule.views_account_editSettings', 'Registered users only'), 2 => Yii::t('UserModule.views_account_editSettings', 'Visible for all (also unregistered users)'), - ]); ?> + ]); + ?> diff --git a/protected/humhub/modules/user/views/auth/login.php b/protected/humhub/modules/user/views/auth/login.php index c071e41a2f..2d6f764d23 100644 --- a/protected/humhub/modules/user/views/auth/login.php +++ b/protected/humhub/modules/user/views/auth/login.php @@ -18,6 +18,13 @@ $this->pageTitle = Yii::t('UserModule.views_auth_login', 'Login');

    + session->hasFlash('error')): ?> + + + + 'account-login-form']); ?>

    field($model, 'username')->textInput(['id' => 'login_username', 'placeholder' => Yii::t('UserModule.views_auth_login', 'username or email')]); ?> @@ -39,6 +46,7 @@ $this->pageTitle = Yii::t('UserModule.views_auth_login', 'Login');
    +
    @@ -56,9 +64,8 @@ $this->pageTitle = Yii::t('UserModule.views_auth_login', 'Login');

    - 'account-register-form']); ?> - - field($registerModel, 'email')->textInput(['id' => 'register-email', 'placeholder' => Yii::t('UserModule.views_auth_login', 'email')]); ?> + 'invite-form']); ?> + field($invite, 'email')->textInput(['id' => 'register-email', 'placeholder' => Yii::t('UserModule.views_auth_login', 'email')]); ?>
    'btn btn-primary')); ?> @@ -86,7 +93,7 @@ $this->pageTitle = Yii::t('UserModule.views_auth_login', 'Login'); // Shake panel after wrong validation -hasErrors()) { ?> +hasErrors()) { ?> $('#register-form').removeClass('bounceInLeft'); $('#register-form').addClass('shake'); $('#login-form').removeClass('bounceIn'); diff --git a/protected/humhub/modules/user/views/auth/login_modal.php b/protected/humhub/modules/user/views/auth/login_modal.php index 1b5a44587e..a2c3abe162 100644 --- a/protected/humhub/modules/user/views/auth/login_modal.php +++ b/protected/humhub/modules/user/views/auth/login_modal.php @@ -16,11 +16,11 @@ use yii\helpers\Url;