diff --git a/Core/Frameworks/Baikal/Core/LDAP.php b/Core/Frameworks/Baikal/Core/LDAP.php new file mode 100644 index 000000000..81a11dd43 --- /dev/null +++ b/Core/Frameworks/Baikal/Core/LDAP.php @@ -0,0 +1,229 @@ + +# (c) 2022-2025 El-Virus +# All rights reserved +# +# http://sabre.io/baikal +# +# This script is part of the Baïkal Server project. The Baïkal +# Server project is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# This copyright notice MUST APPEAR in all copies of the script! +################################################################# + +/** + * This is an authentication backend that uses ldap. + */ +class LDAP extends \Sabre\DAV\Auth\Backend\AbstractBasic { + /** + * Reference to PDO connection. + * + * @var PDO + */ + protected $pdo; + + /** + * PDO table name we'll be using. + * + * @var string + */ + protected $table_name; + + /** + * LDAP Config. + * LDAP Config Struct. + * + * @var \Baikal\Model\Structs\LDAPConfig + */ + protected $ldap_config; + + /** + * Replaces patterns for their assigned value using the + * given username, using cyrus-sasl style replacements. + * + * %u - gets replaced by full username + * %U - gets replaced by user part when the + * username is an email address + * %d - gets replaced by domain part when the + * username is an email address + * %% - gets replaced by % + * %1-9 - gets replaced by parts of the the domain + * split by '.' in reverse order + * + * full example for jane.doe@mail.example.org: + * %u = jane.doe@mail.example.org + * %U = jane.doe + * %d = mail.example.org + * %1 = org + * %2 = example + * %3 = mail + * + * @param string $line + * @param string $username + * + * @return string + */ + public static function patternReplace($line, $username) { + $user_split = [$username]; + $user = $username; + $domain = ''; + try { + $user_split = explode('@', $username, 2); + $user = $user_split[0]; + if (2 == count($user_split)) { + $domain = $user_split[1]; + } + } catch (\Exception $ignored) { + } + $domain_split = []; + try { + $domain_split = array_reverse(explode('.', $domain)); + } catch (\Exception $ignored) { + $domain_split = []; + } + + $parsed_line = ''; + for ($i = 0; $i < strlen($line); ++$i) { + if ('%' == $line[$i]) { + ++$i; + $next_char = $line[$i]; + if ('u' == $next_char) { + $parsed_line .= $username; + } elseif ('U' == $next_char) { + $parsed_line .= $user; + } elseif ('d' == $next_char) { + $parsed_line .= $domain; + } elseif ('%' == $next_char) { + $parsed_line .= '%'; + } else { + for ($j = 1; $j <= count($domain_split) && $j <= 9; ++$j) { + if ($next_char == '' . $j) { + $parsed_line .= $domain_split[$j - 1]; + } + } + } + } else { + $parsed_line .= $line[$i]; + } + } + + return $parsed_line; + } + + /** + * Checks if a user can bind with a password. + * If an error is produced, it will be logged. + * + * @param \LDAP\Connection &$conn + * @param string $dn + * @param string $password + * + * @return bool + */ + public static function doesBind(&$conn, $dn, $password) { + try { + $bind = ldap_bind($conn, $dn, $password); + if ($bind) { + return true; + } + } catch (\ErrorException $e) { + error_log($e->getMessage()); + error_log(ldap_error($conn)); + } + + return false; + } + + /** + * Creates the backend object. + * + * @param \PD0 $pdo + * @param string $table_name + * @param \Baikal\Model\Structs\LDAPConfig $ldap_config + */ + public function __construct(\PDO $pdo, $table_name, $ldap_config) { + $this->pdo = $pdo; + $this->table_name = $table_name; + $this->ldap_config = $ldap_config; + } + + /** + * Connects to an LDAP server and tries to authenticate. + * + * @param string $username + * @param string $password + * + * @return bool + */ + protected function ldapOpen($username, $password) { + try { + $principal = new Principal($username, $this->ldap_config); + } catch (\Exception $ignored) { + return false; + } + + $conn = ldap_connect($this->ldap_config->ldap_uri); + if (!$conn) { + return false; + } + if (!ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3)) { + return false; + } + + $success = $this->doesBind($conn, $principal->dn, $password); + + ldap_close($conn); + + if ($success) { + $stmt = $this->pdo->prepare('SELECT 1 FROM ' . $this->table_name . ' WHERE username = ?'); + $stmt->execute([$username]); + $result = $stmt->fetchAll(); + + if (empty($result)) { + $user = new \Baikal\Model\User(); + $user->set('federation', 'LDAP'); + $user->set('username', $username); + $user->persist(); + } + } + + return $success; + } + + /** + * Validates a username and password by trying to authenticate against LDAP. + * + * @param string $username + * @param string $password + * + * @return bool + */ + protected function validateUserPass($username, $password) { + if (!extension_loaded("ldap")) { + error_log('PHP LDAP extension not enabled'); + + return false; + } + + return $this->ldapOpen($username, $password); + } +} diff --git a/Core/Frameworks/Baikal/Core/Server.php b/Core/Frameworks/Baikal/Core/Server.php index b0baf5a48..208f2fdc4 100644 --- a/Core/Frameworks/Baikal/Core/Server.php +++ b/Core/Frameworks/Baikal/Core/Server.php @@ -27,6 +27,7 @@ namespace Baikal\Core; +use Baikal\Model\Structs\LDAPConfig; use Symfony\Component\Yaml\Yaml; /** @@ -134,6 +135,8 @@ protected function initServer() { $authBackend = new \Baikal\Core\PDOBasicAuth($this->pdo, $this->authRealm); } elseif ($this->authType === 'Apache') { $authBackend = new \Sabre\DAV\Auth\Backend\Apache(); + } elseif ($this->authType === 'LDAP') { + $authBackend = new \Baikal\Core\LDAP($this->pdo, 'users', LDAPConfig::fromArray($config['system'])); } else { $authBackend = new \Sabre\DAV\Auth\Backend\PDO($this->pdo); $authBackend->setRealm($this->authRealm); diff --git a/Core/Frameworks/Baikal/Model/Config/Standard.php b/Core/Frameworks/Baikal/Model/Config/Standard.php index 310d512d4..80028073d 100644 --- a/Core/Frameworks/Baikal/Model/Config/Standard.php +++ b/Core/Frameworks/Baikal/Model/Config/Standard.php @@ -32,17 +32,28 @@ class Standard extends \Baikal\Model\Config { # Default values protected $aData = [ - "configured_version" => BAIKAL_VERSION, - "timezone" => "Europe/Paris", - "card_enabled" => true, - "cal_enabled" => true, - "dav_auth_type" => "Digest", - "admin_passwordhash" => "", - "failed_access_message" => "user %u authentication failure for Baikal", + "configured_version" => BAIKAL_VERSION, + "timezone" => "Europe/Paris", + "card_enabled" => true, + "cal_enabled" => true, + "admin_passwordhash" => "", + "dav_auth_type" => "Digest", + "ldap_mode" => "None", + "ldap_uri" => "ldap://127.0.0.1", + "ldap_bind_dn" => "cn=baikal,ou=apps,dc=example,dc=com", + "ldap_bind_password" => "", + "ldap_dn" => "mail=%u", + "ldap_cn" => "cn", + "ldap_mail" => "mail", + "ldap_search_base" => "ou=users,dc=example,dc=com", + "ldap_search_attribute" => "uid=%U", + "ldap_search_filter" => "(objectClass=*)", + "ldap_group" => "cn=baikal,ou=groups,dc=example,dc=com", + "failed_access_message" => "user %u authentication failure for Baikal", // While not editable as will change admin & any existing user passwords, // could be set to different value when migrating from legacy config - "auth_realm" => "BaikalDAV", - "base_uri" => "", + "auth_realm" => "BaikalDAV", + "base_uri" => "", ]; function __construct() { @@ -76,12 +87,6 @@ function formMorphologyForThisModelInstance() { "help" => "Leave empty to disable sending invite emails", ])); - $oMorpho->add(new \Formal\Element\Listbox([ - "prop" => "dav_auth_type", - "label" => "WebDAV authentication type", - "options" => ["Digest", "Basic", "Apache"], - ])); - $oMorpho->add(new \Formal\Element\Password([ "prop" => "admin_passwordhash", "label" => "Admin password", @@ -93,6 +98,72 @@ function formMorphologyForThisModelInstance() { "validation" => "sameas:admin_passwordhash", ])); + $oMorpho->add(new \Formal\Element\Listbox([ + "prop" => "dav_auth_type", + "label" => "WebDAV authentication type", + "options" => ["Digest", "Basic", "Apache", "LDAP"], + "refreshonchange" => true, + ])); + + $oMorpho->add(new \Formal\Element\Listbox([ + "prop" => "ldap_mode", + "label" => "LDAP authentication mode", + "options" => ["DN", "Attribute", "Filter", "Group"], + "refreshonchange" => true, + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_uri", + "label" => "URI of the LDAP server", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_bind_dn", + "label" => "DN which Baikal will use to bind to the LDAP server", + ])); + + $oMorpho->add(new \Formal\Element\Password([ + "prop" => "ldap_bind_password", + "label" => "Password of the bind DN user", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_dn", + "label" => "User DN for bind", + "help" => "Replacments: %u => username, %U => user part, %d => domain part of username, %1-9 parts of the domain in reverse order", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_cn", + "label" => "LDAP-attribute for displayname", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_mail", + "label" => "LDAP-attribute for email", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_search_base", + "label" => "Base of the LDAP search", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_search_attribute", + "label" => "Attribute to match the user with", + "help" => "Replacments: %u => username, %U => user part, %d => domain part of username, %1-9 parts of the domain in reverse order", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_search_filter", + "label" => "LDAP filter to be applied to the search", + ])); + + $oMorpho->add(new \Formal\Element\Text([ + "prop" => "ldap_group", + "label" => "Group DN that contains the member atribute of the user", + ])); + try { $config = Yaml::parseFile(PROJECT_PATH_CONFIG . "baikal.yaml"); } catch (\Exception $e) { @@ -106,6 +177,7 @@ function formMorphologyForThisModelInstance() { $sNotice = "-- Leave empty to keep current password --"; $oMorpho->element("admin_passwordhash")->setOption("placeholder", $sNotice); $oMorpho->element("admin_passwordhash_confirm")->setOption("placeholder", $sNotice); + $oMorpho->element("ldap_bind_password")->setOption("placeholder", "-- Not Shown --"); } return $oMorpho; @@ -129,11 +201,19 @@ function set($sProp, $sValue) { return $this; } + if ($sProp === "ldap_bind_password" && $sValue === "") { + return; + } + + if (!isset($sValue)) { + return; + } + parent::set($sProp, $sValue); } function get($sProp) { - if ($sProp === "admin_passwordhash" || $sProp === "admin_passwordhash_confirm") { + if ($sProp === "admin_passwordhash" || $sProp === "admin_passwordhash_confirm" || $sProp === "ldap_bind_password") { return ""; } diff --git a/Core/Frameworks/Baikal/Model/Principal.php b/Core/Frameworks/Baikal/Model/Principal/DBPrincipal.php similarity index 91% rename from Core/Frameworks/Baikal/Model/Principal.php rename to Core/Frameworks/Baikal/Model/Principal/DBPrincipal.php index 2b86b1ae4..1cdb84090 100644 --- a/Core/Frameworks/Baikal/Model/Principal.php +++ b/Core/Frameworks/Baikal/Model/Principal/DBPrincipal.php @@ -25,11 +25,12 @@ # This copyright notice MUST APPEAR in all copies of the script! ################################################################# -namespace Baikal\Model; +namespace Baikal\Model\Principal; -class Principal extends \Flake\Core\Model\Db { +class DBPrincipal extends \Flake\Core\Model\Db { const DATATABLE = "principals"; const PRIMARYKEY = "id"; + public const EDITABLE = true; protected $aData = [ "uri" => "", "displayname" => "", diff --git a/Core/Frameworks/Baikal/Model/Principal/LDAP.php b/Core/Frameworks/Baikal/Model/Principal/LDAP.php new file mode 100644 index 000000000..694792aa8 --- /dev/null +++ b/Core/Frameworks/Baikal/Model/Principal/LDAP.php @@ -0,0 +1,185 @@ + +# All rights reserved +# +# http://sabre.io/baikal +# +# This script is part of the Baïkal Server project. The Baïkal +# Server project is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# This copyright notice MUST APPEAR in all copies of the script! +################################################################# + +namespace Baikal\Model\Principal; + +use Baikal\Core\LDAP as LDAPCore; +use Baikal\Model\Structs\LDAPConfig; +use Symfony\Component\Yaml\Yaml; + +/** @phpstan-consistent-constructor */ +class LDAP extends DBPrincipal { + public const EDITABLE = false; + public readonly string $dn; + + protected $aData = [ + "uri" => "", + "displayname" => "", + "email" => "", + ]; + + public function __construct($username, $ldap_config = null) { + if (!isset($ldap_config)) { + $config = Yaml::parseFile(PROJECT_PATH_CONFIG . 'baikal.yaml'); + $ldap_config = LDAPConfig::fromArray($config['system']); + unset($config); + } + + $conn = ldap_connect($ldap_config->ldap_uri); + if (!$conn) { + throw new \Exception('LDAP connect failed'); + } + if (!ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3)) { + throw new \Exception('LDAP server does not support protocol version 3'); + } + + try { + switch ($ldap_config->ldap_mode) { + case 'DN': + $this->dn = LDAPCore::patternReplace($ldap_config->ldap_dn, $username); + break; + + case 'Attribute': + case 'Group': + try { + if (!LDAPCore::doesBind($conn, $ldap_config->ldap_bind_dn, $ldap_config->ldap_bind_password)) { + throw new \Exception('LDAP Service user fails to bind'); + } + + $attribute = $ldap_config->ldap_search_attribute; + $attribute = LDAPCore::patternReplace($attribute, $username); + + $result = ldap_get_entries($conn, ldap_search($conn, $ldap_config->ldap_search_base, '(' . $attribute . ')', + [$ldap_config->ldap_search_attribute], 0, 1, 0, LDAP_DEREF_ALWAYS, []))[0]; + + if ((!isset($result)) || (!isset($result['dn']))) { + throw new \Exception('No LDAP entry matches Attribute'); + } + + if ($ldap_config->ldap_mode == 'Group') { + $inGroup = false; + $members = ldap_get_entries($conn, ldap_read($conn, $ldap_config->ldap_group, '(objectClass=*)', + ['member', 'uniqueMember'], 0, 0, 0, LDAP_DEREF_NEVER, []))[0]; + if (isset($members['member'])) { + foreach ($members['member'] as $member) { + if ($member == $result['dn']) { + $inGroup = true; + break; + } + } + } + if (isset($members['uniqueMember'])) { + foreach ($members['uniqueMember'] as $member) { + if ($member == $result['dn']) { + $inGroup = false; + break; + } + } + } + if (!$inGroup) { + throw new \Exception('The user is not in the specified Group'); + } + } + + $this->dn = $result['dn']; + } catch (\ErrorException $e) { + error_log($e->getMessage()); + error_log(ldap_error($conn)); + throw new \Exception('LDAP error'); + } + break; + + case 'Filter': + try { + if (!LDAPCore::doesBind($conn, $ldap_config->ldap_bind_dn, $ldap_config->ldap_bind_password)) { + throw new \Exception('LDAP Service user fails to bind'); + } + + $filter = $this->ldap_config->ldap_search_filter; + $filter = LDAPCore::patternReplace($filter, $username); + + $result = ldap_get_entries($conn, ldap_search($conn, $ldap_config->ldap_search_base, $filter, [], 0, 1, 0, LDAP_DEREF_ALWAYS, []))[0]; + + $this->dn = $result['dn']; + } catch (\ErrorException $e) { + error_log($e->getMessage()); + error_log(ldap_error($conn)); + throw new \Exception('LDAP error'); + } + break; + + default: + error_log('Unknown LDAP authentication mode'); + throw new \Exception('Unknown LDAP authentication mode'); + } + + $results = ldap_read($conn, $this->dn, '(objectclass=*)', [$ldap_config->ldap_cn, $ldap_config->ldap_mail]); + $entry = ldap_get_entries($conn, $results)[0]; + $displayname = $username; + $email = 'unset-email'; + if (!empty($entry[$ldap_config->ldap_cn])) { + $displayname = $entry[$ldap_config->ldap_cn][0]; + } + if (!empty($entry[$ldap_config->ldap_mail])) { + $email = $entry[$ldap_config->ldap_mail][0]; + } + + parent::set('uri', 'principals/' . $username); + parent::set('displayname', $displayname); + parent::set('email', $email); + } finally { + ldap_close($conn); + } + } + + static function fromPrincipal($db_principal, $username) { + $principal = new static($username); + + if (isset($db_principal)) { + $principal->aData[$db_principal->getPrimaryKey()] = $db_principal->getPrimary(); + $principal->bFloating = false; + } + + $principal->persist(); + + return $principal; + } + + function set($sPropName, $sPropValue) { + if (!array_key_exists($sPropName, $this->aData)) { + parent::set($sPropName, $sPropValue); + } + } + + function persist() { + return parent::persist(); + } + + function destroy() { + return parent::destroy(); + } +} diff --git a/Core/Frameworks/Baikal/Model/Structs/LDAPConfig.php b/Core/Frameworks/Baikal/Model/Structs/LDAPConfig.php new file mode 100644 index 000000000..b44eb1601 --- /dev/null +++ b/Core/Frameworks/Baikal/Model/Structs/LDAPConfig.php @@ -0,0 +1,63 @@ + +# All rights reserved +# +# http://sabre.io/baikal +# +# This script is part of the Baïkal Server project. The Baïkal +# Server project is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# The GNU General Public License can be found at +# http://www.gnu.org/copyleft/gpl.html. +# +# This script is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# This copyright notice MUST APPEAR in all copies of the script! +################################################################# + +namespace Baikal\Model\Structs; + +/** + * Struct that holds the Configuration parameters for LDAP authentication. + */ +final class LDAPConfig { + public $ldap_mode; + public $ldap_uri; + public $ldap_bind_dn; + public $ldap_bind_password; + public $ldap_dn; + public $ldap_cn; + public $ldap_mail; + public $ldap_search_base; + public $ldap_search_attribute; + public $ldap_search_filter; + public $ldap_group; + + public static function fromArray($array) { + $LDAPConfig = new static(); + + $LDAPConfig->ldap_mode = $array['ldap_mode']; + $LDAPConfig->ldap_uri = $array['ldap_uri']; + $LDAPConfig->ldap_bind_dn = $array['ldap_bind_dn']; + $LDAPConfig->ldap_bind_password = $array['ldap_bind_password']; + $LDAPConfig->ldap_dn = $array['ldap_dn']; + $LDAPConfig->ldap_cn = $array['ldap_cn']; + $LDAPConfig->ldap_mail = $array['ldap_mail']; + $LDAPConfig->ldap_search_base = $array['ldap_search_base']; + $LDAPConfig->ldap_search_attribute = $array['ldap_search_attribute']; + $LDAPConfig->ldap_search_filter = $array['ldap_search_filter']; + $LDAPConfig->ldap_group = $array['ldap_group']; + + return $LDAPConfig; + } +} diff --git a/Core/Frameworks/Baikal/Model/User.php b/Core/Frameworks/Baikal/Model/User.php index 4bb9ade97..8f946d859 100644 --- a/Core/Frameworks/Baikal/Model/User.php +++ b/Core/Frameworks/Baikal/Model/User.php @@ -35,6 +35,7 @@ class User extends \Flake\Core\Model\Db { const LABELFIELD = "username"; protected $aData = [ + "federation" => null, "username" => "", "digesta1" => "", ]; @@ -45,10 +46,23 @@ function initByPrimary($sPrimary) { parent::initByPrimary($sPrimary); # Initializing principals - $this->oIdentityPrincipal = \Baikal\Model\Principal::getBaseRequester() + $dbPrincipal = \Baikal\Model\Principal\DBPrincipal::getBaseRequester() ->addClauseEquals("uri", "principals/" . $this->get("username")) ->execute() ->first(); + + switch (parent::get("federation")) { + case null: + $this->oIdentityPrincipal = $dbPrincipal; + break; + + case "LDAP": + $this->oIdentityPrincipal = \Baikal\Model\Principal\LDAP::fromPrincipal($dbPrincipal, $this->get("username")); + break; + + default: + throw new \Exception("Unknown user federation"); + } } function getAddressBooksBaseRequester() { @@ -75,7 +89,7 @@ function initFloating() { parent::initFloating(); # Initializing principals - $this->oIdentityPrincipal = new \Baikal\Model\Principal(); + $this->oIdentityPrincipal = new \Baikal\Model\Principal\DBPrincipal(); } function get($sPropName) { @@ -287,4 +301,8 @@ function getPasswordHashForPassword($sPassword) { return md5($this->get("username") . ':' . $config['system']['auth_realm'] . ':' . $sPassword); } + + function isEditable() { + return $this->oIdentityPrincipal::class::EDITABLE; + } } diff --git a/Core/Frameworks/BaikalAdmin/Controller/Install/Initialize.php b/Core/Frameworks/BaikalAdmin/Controller/Install/Initialize.php index f1cc64393..10f4b29df 100644 --- a/Core/Frameworks/BaikalAdmin/Controller/Install/Initialize.php +++ b/Core/Frameworks/BaikalAdmin/Controller/Install/Initialize.php @@ -62,6 +62,7 @@ function execute() { $this->oForm = $this->oModel->formForThisModelInstance([ "close" => false, + "hook.morphology" => [new \BaikalAdmin\Controller\Settings\Standard(), "morphologyHook"], ]); if ($this->oForm->submitted()) { diff --git a/Core/Frameworks/BaikalAdmin/Controller/Settings/Standard.php b/Core/Frameworks/BaikalAdmin/Controller/Settings/Standard.php index 204f31b87..1473c488f 100644 --- a/Core/Frameworks/BaikalAdmin/Controller/Settings/Standard.php +++ b/Core/Frameworks/BaikalAdmin/Controller/Settings/Standard.php @@ -27,6 +27,8 @@ namespace BaikalAdmin\Controller\Settings; +use Symfony\Component\Yaml\Yaml; + class Standard extends \Flake\Core\Controller { /** * @var \Baikal\Model\Config\Standard @@ -48,6 +50,7 @@ function execute() { $this->oForm = $this->oModel->formForThisModelInstance([ "close" => false, + "hook.morphology" => [$this, "morphologyHook"], ]); if ($this->oForm->submitted()) { @@ -61,4 +64,55 @@ function render() { return $oView->render(); } + + function morphologyHook(\Formal\Form $oForm, \Formal\Form\Morphology $oMorpho) { + if ($oForm->submitted()) { + $bLDAP = (strval($oForm->postValue("dav_auth_type")) === "LDAP"); + $sLDAPm = strval($oForm->postValue("ldap_mode")); + } else { + try { + $config = Yaml::parseFile(PROJECT_PATH_CONFIG . "baikal.yaml"); + } catch (\Exception $e) { + error_log('Error reading baikal.yaml file : ' . $e->getMessage()); + } + $bLDAP = isset($config['system']['dav_auth_type']) ? ($config['system']['dav_auth_type'] === 'LDAP') : false; + $sLDAPm = $config['system']['ldap_mode'] ?? 'DN'; + } + + if ($bLDAP) { + if ($sLDAPm === "DN") { + $oMorpho->remove("ldap_bind_dn"); + $oMorpho->remove("ldap_bind_password"); + $oMorpho->remove("ldap_search_base"); + $oMorpho->remove("ldap_search_attribute"); + $oMorpho->remove("ldap_search_filter"); + $oMorpho->remove("ldap_group"); + } elseif ($sLDAPm === "Attribute") { + $oMorpho->remove("ldap_dn"); + $oMorpho->remove("ldap_search_filter"); + $oMorpho->remove("ldap_group"); + } elseif ($sLDAPm === "Filter") { + $oMorpho->remove("ldap_dn"); + $oMorpho->remove("ldap_search_attribute"); + $oMorpho->remove("ldap_group"); + } elseif ($sLDAPm === "Group") { + $oMorpho->remove("ldap_dn"); + $oMorpho->remove("ldap_search_filter"); + } else { + error_log('Unknown LDAP mode: ' . $sLDAPm); + } + } else { + $oMorpho->remove("ldap_uri"); + $oMorpho->remove("ldap_mode"); + $oMorpho->remove("ldap_bind_dn"); + $oMorpho->remove("ldap_bind_password"); + $oMorpho->remove("ldap_dn"); + $oMorpho->remove("ldap_cn"); + $oMorpho->remove("ldap_mail"); + $oMorpho->remove("ldap_search_base"); + $oMorpho->remove("ldap_search_attribute"); + $oMorpho->remove("ldap_search_filter"); + $oMorpho->remove("ldap_group"); + } + } } diff --git a/Core/Frameworks/BaikalAdmin/Controller/Users.php b/Core/Frameworks/BaikalAdmin/Controller/Users.php index 6ef90c039..6e638d7f6 100644 --- a/Core/Frameworks/BaikalAdmin/Controller/Users.php +++ b/Core/Frameworks/BaikalAdmin/Controller/Users.php @@ -71,6 +71,7 @@ function render() { "username" => $user->get("username"), "displayname" => $user->get("displayname"), "email" => $user->get("email"), + "federation" => $user->get("federation"), ]; } @@ -120,10 +121,12 @@ protected function actionEditRequested() { protected function actionEdit() { $aParams = $this->getParams(); $this->oModel = new \Baikal\Model\User(intval($aParams["edit"])); - $this->initForm(); + if ($this->oModel->isEditable()) { + $this->initForm(); - if ($this->oForm->submitted()) { - $this->oForm->execute(); + if ($this->oForm->submitted()) { + $this->oForm->execute(); + } } } @@ -217,6 +220,10 @@ function linkNew() { } static function linkEdit(\Baikal\Model\User $user) { + if (!$user->isEditable()) { + return null; + } + return self::buildRoute([ "edit" => $user->get("id"), ]) . "#form"; diff --git a/Core/Frameworks/BaikalAdmin/Resources/Templates/Users.html b/Core/Frameworks/BaikalAdmin/Resources/Templates/Users.html index 042234739..af81b0671 100644 --- a/Core/Frameworks/BaikalAdmin/Resources/Templates/Users.html +++ b/Core/Frameworks/BaikalAdmin/Resources/Templates/Users.html @@ -10,6 +10,9 @@

Users

{{ user.username|escape }}
+ {% if user.federation %} + {{ user.federation }} + {% endif %} {{ user.displayname|escape }} <{{ user.email|escape }}> @@ -17,7 +20,9 @@

Users

Calendars Address Books + {% if user.linkedit %} Edit + {% endif %} Delete

diff --git a/Core/Resources/Db/MySQL/db.sql b/Core/Resources/Db/MySQL/db.sql index 51757cda0..0342101d4 100644 --- a/Core/Resources/Db/MySQL/db.sql +++ b/Core/Resources/Db/MySQL/db.sql @@ -140,6 +140,7 @@ CREATE TABLE propertystorage ( CREATE UNIQUE INDEX path_property ON propertystorage (path(600), name(100)); CREATE TABLE users ( id INTEGER UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + federation VARCHAR(20), username VARBINARY(50), digesta1 VARBINARY(32), UNIQUE(username) diff --git a/Core/Resources/Db/SQLite/db.sql b/Core/Resources/Db/SQLite/db.sql index 6d3bf7c73..22d91c944 100644 --- a/Core/Resources/Db/SQLite/db.sql +++ b/Core/Resources/Db/SQLite/db.sql @@ -141,6 +141,7 @@ CREATE TABLE propertystorage ( CREATE UNIQUE INDEX path_property ON propertystorage (path, name); CREATE TABLE users ( id integer primary key asc NOT NULL, + federation TEXT, username TEXT NOT NULL, digesta1 TEXT NOT NULL, UNIQUE(username) diff --git a/config/baikal.yaml.dist b/config/baikal.yaml.dist index 3fd772b59..a60113445 100644 --- a/config/baikal.yaml.dist +++ b/config/baikal.yaml.dist @@ -9,6 +9,17 @@ system: failed_access_message: 'user %u authentication failure for Baikal' auth_realm: BaikalDAV base_uri: '' + ldap_mode: 'None' + ldap_uri: 'ldap://127.0.0.1' + ldap_bind_dn: 'cn=baikal,ou=apps,dc=example,dc=com' + ldap_bind_password: '' + ldap_dn: 'mail=%u' + ldap_cn: 'cn' + ldap_mail: 'mail' + ldap_search_base: 'ou=users,dc=example,dc=com' + ldap_search_attribute: 'uid=%U' + ldap_search_filter: '(objectClass=*)' + ldap_group: 'cn=baikal,ou=groups,dc=example,dc=com' database: encryption_key: 5d3f0fa0192e3058ea70f1bb20924add sqlite_file: "absolute/path/to/Specific/db/db.sqlite"