diff --git a/README.md b/README.md index d77899a..f456f97 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ LDAP settings: * hostname : localhost * port : 389 +* encryption : none +* username : put something random +* password : put something random * baseDn : ou=users,dc=yunohost,dc=org * loginFilter : (uid=%s) * userFilter : objectClass=mailAccount diff --git a/conf/nginx.conf b/conf/nginx.conf index fbf7db9..b1fbc6a 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -1,5 +1,5 @@ -location YNH_WWW_PATH { - alias YNH_WWW_ALIAS ; +location YNH_WWW_PATH/ { + alias YNH_WWW_ALIAS; index index.php; try_files $uri $uri/ index.php?$args; @@ -13,18 +13,18 @@ location YNH_WWW_PATH { fastcgi_param SCRIPT_FILENAME $request_filename; } - location ~ ^YNH_WWW_PATH(?:ico|css|js|gif|jpe?g|png|ttf|woff)$ { + location ~ ^YNH_WWW_PATH/(?:ico|css|js|gif|jpe?g|png|ttf|woff)$ { access_log off; expires 30d; add_header Pragma public; add_header Cache-Control "public, mustrevalidate, proxy-revalidate"; } - location = YNH_WWW_PATHprotected { + location = YNH_WWW_PATH/protected { deny all; } - location = YNH_WWW_PATHuploads/file { + location = YNH_WWW_PATH/uploads/file { deny all; } diff --git a/scripts/install b/scripts/install index 9522165..ac3d87e 100644 --- a/scripts/install +++ b/scripts/install @@ -4,7 +4,8 @@ set -eu app=$YNH_APP_INSTANCE_NAME -version='1.1.0' +version='1.2.0' +source='https://github.com/humhub/humhub' # Retrieve arguments domain=$YNH_APP_ARG_DOMAIN @@ -13,6 +14,14 @@ version='1.1.0' # Source YunoHost helpers source /usr/share/yunohost/helpers +# Correct path: puts a / at the start and nothing at the end + if [ "${path:0:1}" != "/" ]; then + path="/$path" + fi + if [ "${path:${#path}-1}" == "/" ] && [ ${#path} -gt 1 ]; then + path="${path:0:${#path}-1}" + fi + # Check domain/path availability sudo yunohost app checkurl "${domain}${path}" -a "$app" \ || ynh_die "Path not available: ${domain}${path}" @@ -20,8 +29,14 @@ version='1.1.0' # Copy source files src_path=/var/www/$app sudo mkdir -p $src_path - sudo unzip -qq ../sources/humhub-$version.zip - sudo cp -a humhub-$version/. $src_path + sudo unzip -qq ../sources/humhub-${version}.zip + sudo cp -a humhub-${version}/. $src_path + +# Hotfixes + # Fix LDAP email. See https://github.com/humhub/humhub/issues/1949 + sudo cp -a ../sources/fix/AuthClientHelpers.php $src_path/protected/humhub/modules/user/authclient/AuthClientHelpers.php + # Fix to allow passwordless LDAP login + sudo cp -a ../sources/fix/ZendLdapClient.php $src_path/protected/humhub/modules/user/authclient/ZendLdapClient.php # MySQL dbuser=$app @@ -48,9 +63,10 @@ version='1.1.0' # Modify Nginx configuration file and copy it to Nginx conf directory nginx_conf=../conf/nginx.conf - sed -i "s@YNH_WWW_PATH@$path@g" $nginx_conf + sed -i "s@YNH_WWW_PATH@${path:-/}@g" $nginx_conf sed -i "s@YNH_WWW_ALIAS@$src_path/@g" $nginx_conf sudo cp $nginx_conf /etc/nginx/conf.d/$domain.d/$app.conf # Reload services + sudo service php5-fpm reload sudo service nginx reload diff --git a/scripts/upgrade b/scripts/upgrade index ef07eac..51765ec 100644 --- a/scripts/upgrade +++ b/scripts/upgrade @@ -4,7 +4,7 @@ set -eu app=$YNH_APP_INSTANCE_NAME -version='1.1.0' +version='1.2.0' # Source YunoHost helpers source /usr/share/yunohost/helpers @@ -12,19 +12,39 @@ version='1.1.0' # Retrieve app settings domain=$(ynh_app_setting_get "$app" domain) path=$(ynh_app_setting_get "$app" path) + dbuser=$app + dbname=$app + dbpass=$(ynh_app_setting_get "$app" mysqlpwd) -# Remove trailing "/" for next commands - path=${path%/} +# Correct path: puts a / at the start and nothing at the end + if [ "${path:0:1}" != "/" ]; then + path="/$path" + fi + if [ "${path:${#path}-1}" == "/" ] && [ ${#path} -gt 1 ]; then + path="${path:0:${#path}-1}" + fi # Copy source files - src_path=/var/www/$app sudo mkdir -p $src_path sudo unzip -qq ../sources/humhub-$version.zip sudo cp -a humhub-$version/. $src_path +# Conf + app_conf=../conf/common.php + sed -i "s@DBNAME_TO_CHANGE@$dbname@g" $app_conf + sed -i "s@DBUSER_TO_CHANGE@$dbuser@g" $app_conf + sed -i "s@DBPASS_TO_CHANGE@$dbpass@g" $app_conf + sudo cp $app_conf $src_path/protected/config/common.php + +# Hotfixes + # Fix LDAP email. See https://github.com/humhub/humhub/issues/1949 + sudo cp -a ../sources/fix/AuthClientHelpers.php $src_path/protected/humhub/modules/user/authclient/AuthClientHelpers.php + # Fix to allow passwordless LDAP login + sudo cp -a ../sources/fix/ZendLdapClient.php $src_path/protected/humhub/modules/user/authclient/ZendLdapClient.php + # Set permissions to app files sudo chown -R www-data: $src_path - + # Cron echo "30 * * * * $src_path/protected/yii cron hourly >/dev/null 2>&1" > cron echo "00 18 * * * $src_path/protected/yii cron daily >/dev/null 2>&1" > cron @@ -33,9 +53,10 @@ version='1.1.0' # Modify Nginx configuration file and copy it to Nginx conf directory nginx_conf=../conf/nginx.conf - sed -i "s@YNH_WWW_PATH@$path@g" $nginx_conf + sed -i "s@YNH_WWW_PATH@${path:-/}@g" $nginx_conf sed -i "s@YNH_WWW_ALIAS@$src_path/@g" $nginx_conf sudo cp $nginx_conf /etc/nginx/conf.d/$domain.d/$app.conf # Reload nginx service + sudo service php5-fpm reload sudo service nginx reload diff --git a/sources/fix/AuthClientHelpers.php b/sources/fix/AuthClientHelpers.php new file mode 100644 index 0000000..9e0766b --- /dev/null +++ b/sources/fix/AuthClientHelpers.php @@ -0,0 +1,261 @@ +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(['source' => $authClient->getId(), 'source_id' => $attributes['id']]); + + /** + * Make sure authClient is not double assigned + */ + if ($auth !== null && $auth->user_id != $user->id) { + $auth->delete(); + $auth = null; + } + + + 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]); + } + } else { + if ($user->profile->hasAttribute($attributeName)) { + $user->profile->setAttribute($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->profile->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; + } + + // Hotfix for YunoHost. Select the first LDAP email address when there are several in the mail attribute. See https://github.com/humhub/humhub/issues/1949 + if (is_array($attributes['mail'])) { + $attributes['mail'] = $attributes['mail'][0]; + } + if (is_array($attributes['email'])) { + $attributes['email'] = $attributes['email'][0]; + } + + $registration = new \humhub\modules\user\models\forms\Registration(); + $registration->enablePasswordForm = false; + $registration->enableEmailField = true; + + if ($authClient instanceof interfaces\ApprovalBypass) { + $registration->enableUserApproval = false; + } + + unset($attributes['id']); + $registration->getUser()->setAttributes($attributes, false); + $registration->getProfile()->setAttributes($attributes, false); + $registration->getGroupUser()->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/sources/fix/ZendLdapClient.php b/sources/fix/ZendLdapClient.php new file mode 100644 index 0000000..2e75f09 --- /dev/null +++ b/sources/fix/ZendLdapClient.php @@ -0,0 +1,380 @@ +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 = Yii::$app->getModule('user')->settings->get('auth.ldap.usernameAttribute'); + if ($usernameAttribute == '') { + $usernameAttribute = 'sAMAccountName'; + } + $map['username'] = strtolower($usernameAttribute); + + // E-Mail field + $emailAttribute = Yii::$app->getModule('user')->settings->get('auth.ldap.emailAttribute'); + 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 && $name != 'memberof') { + $normalized[$name] = $value[0]; + } else { + $normalized[$name] = $value; + } + } + + if (isset($normalized['objectguid'])) { + $normalized['objectguid'] = \humhub\libs\StringHelper::binaryToGuid($normalized['objectguid']); + } + + // Handle date fields (formats are specified in config) + foreach ($normalized as $name => $value) { + if (isset(Yii::$app->params['ldap']['dateFields'][$name]) && $value != '') { + $dateFormat = Yii::$app->params['ldap']['dateFields'][$name]; + $date = \DateTime::createFromFormat($dateFormat, $value); + + if ($date !== false) { + $normalized[$name] = $date->format('Y-m-d 00:00:00'); + } else { + $normalized[$name] = ""; + } + } + } + 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() + { + $userName = $this->login->username; + + // Translate given e-mail to username + if (strpos($userName, '@') !== false) { + $user = User::findOne(['email' => $userName]); + if ($user !== null) { + $userName = $user->username; + } + } + + try { + $this->getLdap()->bind($userName, $this->login->password); + return $this->getLdap()->getCanonicalAccountName($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' => Yii::$app->getModule('user')->settings->get('auth.ldap.hostname'), + 'port' => Yii::$app->getModule('user')->settings->get('auth.ldap.port'), + //'username' => Yii::$app->getModule('user')->settings->get('auth.ldap.username'), + //'password' => Yii::$app->getModule('user')->settings->get('auth.ldap.password'), + 'username' => '', + 'password' => '', + 'useStartTls' => (Yii::$app->getModule('user')->settings->get('auth.ldap.encryption') == 'tls'), + 'useSsl' => (Yii::$app->getModule('user')->settings->get('auth.ldap.encryption') == 'ssl'), + 'bindRequiresDn' => true, + 'baseDn' => Yii::$app->getModule('user')->settings->get('auth.ldap.baseDn'), + 'accountFilterFormat' => Yii::$app->getModule('user')->settings->get('auth.ldap.loginFilter'), + ); + + $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 (!Yii::$app->getModule('user')->settings->get('auth.ldap.enabled') || !Yii::$app->getModule('user')->settings->get('auth.ldap.refreshUsers')) { + return; + } + + $userFilter = Yii::$app->getModule('user')->settings->get('auth.ldap.userFilter'); + $baseDn = Yii::$app->getModule('user')->settings->get('auth.ldap.baseDn'); + try { + $ldap = $this->getLdap(); + + $userCollection = $ldap->search($userFilter, $baseDn, Ldap::SEARCH_SCOPE_SUB); + + $authClient = null; + $ids = []; + foreach ($userCollection as $attributes) { + $authClient = clone $this; + $authClient->init(); + $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 - check required attributes! (' . print_r($attributes, 1) . ')'); + } + } 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(); + } + + foreach (AuthClientHelpers::getUsersByAuthClient($this)->each() as $user) { + $foundInLdap = in_array($user->getAttribute($userTableIdAttribute), $ids); + // Enable disabled users that have been found in ldap + if ($foundInLdap && $user->status === User::STATUS_DISABLED) { + $user->status = User::STATUS_ENABLED; + $user->save(); + Yii::info('Enabled user' . $user->username . ' (' . $user->id . ') - found in LDAP!'); + // Disable users that were not found in ldap + } elseif (!$foundInLdap && $user->status !== User::STATUS_DISABLED) { + $user->status = User::STATUS_DISABLED; + $user->save(); + Yii::warning('Disabled user' . $user->username . ' (' . $user->id . ') - not found in LDAP!'); + } + } + } catch (\Zend\Ldap\Exception\LdapException $ex) { + Yii::error('Could not connect to LDAP instance: ' . $ex->getMessage()); + } catch (\Exception $ex) { + Yii::error('An error occurred while user sync: ' . $ex->getMessage()); + } + } + + /** + * 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/sources/humhub-1.1.0.zip b/sources/humhub-1.2.0.zip similarity index 59% rename from sources/humhub-1.1.0.zip rename to sources/humhub-1.2.0.zip index d4cd076..da9d264 100644 Binary files a/sources/humhub-1.1.0.zip and b/sources/humhub-1.2.0.zip differ