diff --git a/plugins/ldap-identities/LdapConfig.php b/plugins/ldap-identities/LdapConfig.php new file mode 100644 index 000000000..b11483875 --- /dev/null +++ b/plugins/ldap-identities/LdapConfig.php @@ -0,0 +1,67 @@ +server = trim($config->Get("plugin", self::CONFIG_SERVER)); + $ldap->protocol = (int) trim($config->Get("plugin", self::CONFIG_PROTOCOL_VERSION, 3)); + $ldap->bind_user = trim($config->Get("plugin", self::CONFIG_BIND_USER)); + $ldap->bind_password = trim($config->Get("plugin", self::CONFIG_BIND_PASSWORD)); + $ldap->user_base = trim($config->Get("plugin", self::CONFIG_USER_BASE)); + $ldap->user_objectclass = trim($config->Get("plugin", self::CONFIG_USER_OBJECTCLASS)); + $ldap->user_field_name = trim($config->Get("plugin", self::CONFIG_USER_FIELD_NAME)); + $ldap->user_field_search = trim($config->Get("plugin", self::CONFIG_USER_FIELD_SEARCH)); + $ldap->user_field_mail = trim($config->Get("plugin", self::CONFIG_USER_FIELD_MAIL)); + $ldap->group_get = (bool) trim($config->Get("plugin", self::CONFIG_GROUP_GET)); + $ldap->group_base = trim($config->Get("plugin", self::CONFIG_GROUP_BASE)); + $ldap->group_objectclass = trim($config->Get("plugin", self::CONFIG_GROUP_OBJECTCLASS)); + $ldap->group_field_name = trim($config->Get("plugin", self::CONFIG_GROUP_FIELD_NAME)); + $ldap->group_field_member = trim($config->Get("plugin", self::CONFIG_GROUP_FIELD_MEMBER)); + $ldap->group_field_mail = trim($config->Get("plugin", self::CONFIG_GROUP_FIELD_MAIL)); + $ldap->group_sender_format = trim($config->Get("plugin", self::CONFIG_GROUP_SENDER_FORMAT)); + + return $ldap; + } +} \ No newline at end of file diff --git a/plugins/ldap-identities/LdapException.php b/plugins/ldap-identities/LdapException.php new file mode 100644 index 000000000..2abaf07a8 --- /dev/null +++ b/plugins/ldap-identities/LdapException.php @@ -0,0 +1,5 @@ +config = $config; + $this->logger = $logger; + + // Check if LDAP is available + if(!extension_loaded('ldap') || !function_exists('ldap_connect')) { + $this->ldapAvailable = false; + $logger->Write("The LDAP plugin is not available!", Type::WARNING, self::LOG_KEY); + return; + } + + $this->Connect(); + } + + /** + * @inheritDoc + */ + public function GetIdentities(Account $account): array + { + try { + $this->EnsureBound(); + } catch(LdapException $e) { + return []; // exceptions are only thrown from the handleerror function that does logging already + } + + $identities = []; + + // Try and get identity information + $username = @ldap_escape($account->Email(), "", LDAP_ESCAPE_FILTER); + + try { + $userResults = $this->FindLdapResults( + $this->config->user_field_search, + $username, + $this->config->user_base, + $this->config->user_objectclass, + $this->config->user_field_name, + $this->config->user_field_mail + ); + } catch (LdapException $e) { + return []; // exceptions are only thrown from the handleerror function that does logging already + } + + if(count($userResults) < 1) { + $this->logger->Write("Could not find user $username", Type::NOTICE, self::LOG_KEY); + return []; + } else if(count($userResults) > 1) { + $this->logger->Write("Found multiple matches for user $username", Type::WARNING, self::LOG_KEY); + } + + $userResult = $userResults[0]; + + foreach($userResult->emails as $email) { + $identity = new Identity($email, $email); + $identity->SetName($userResult->name); + + if($email === $account->Email()) + $identity->SetId(""); // primary identity + + $identities[] = $identity; + } + + try { + $groupResults = $this->FindLdapResults( + $this->config->group_field_member, + $userResult->dn, + $this->config->group_base, + $this->config->group_objectclass, + $this->config->group_field_name, + $this->config->group_field_mail + ); + } catch (LdapException $e) { + return []; // exceptions are only thrown from the handleerror function that does logging already + } + + foreach($groupResults as $group) { + foreach($group->emails as $email) { + $name = $this->config->group_sender_format; + $name = str_replace("#USER#", $userResult->name, $name); + $name = str_replace("#GROUP#", $group->name, $name); + + $identity = new Identity($email, $email); + $identity->SetName($name); + $identity->SetBcc($email); + + $identities[] = $identity; + } + } + + return $identities; + } + + /** + * @inheritDoc + * @throws \RainLoop\Exceptions\Exception + */ + public function SetIdentities(Account $account, array $identities) + { + throw new \RainLoop\Exceptions\Exception("Ldap identities provider does not support storage"); + } + + /** + * @inheritDoc + */ + public function SupportsStore(): bool + { + return false; + } + + /** + * @inheritDoc + */ + public function Name(): string + { + return "Ldap"; + } + + /** @throws LdapException */ + private function EnsureConnected() : void { + if($this->ldapConnected) return; + + $res = $this->Connect(); + if(!$res) + $this->HandleLdapError("Connect"); + } + private function Connect() : bool { + // Set up connection + $ldap = @ldap_connect($this->config->server); + if($ldap === false) { + $this->ldapAvailable = false; + return false; + } + + // Set protocol version + $option = @ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, $this->config->protocol); + if(!$option) { + $this->ldapAvailable = false; + return false; + } + + $this->ldap = $ldap; + $this->ldapConnected = true; + return true; + } + + /** @throws LdapException */ + private function EnsureBound() : void { + if($this->ldapBound) return; + $this->EnsureConnected(); + + $res = $this->Bind(); + if(!$res) + $this->HandleLdapError("Bind"); + } + private function Bind() : bool { + // Bind to LDAP here + $bindResult = @ldap_bind($this->ldap, $this->config->bind_user, $this->config->bind_password); + if(!$bindResult) { + $this->ldapAvailable = false; + return false; + } + + $this->ldapBound = true; + return true; + } + + /** + * @param string $op + * @throws LdapException + */ + private function HandleLdapError(string $op = ""): void { + // Obtain LDAP error and write logs + $errorNo = @ldap_errno($this->ldap); + $errorMsg = @ldap_error($this->ldap); + + $message = empty($op) ? "LDAP Error: {$errorMsg} ({$errorNo})" : "LDAP Error during {$op}: {$errorMsg} ({$errorNo})"; + $this->logger->Write($message, Type::ERROR, self::LOG_KEY); + throw new LdapException($message, $errorNo); + } + + /** + * @param string $searchField + * @param string $searchValue + * @param string $searchBase + * @param string $objectClass + * @param string $nameField + * @param string $mailField + * @return LdapResult[] + * @throws LdapException + */ + private function FindLdapResults(string $searchField, string $searchValue, string $searchBase, string $objectClass, string $nameField, string $mailField) : array { + $this->EnsureBound(); + + $nameField = strtolower($nameField); + $mailField = strtolower($mailField); + + $filter = "(&(objectclass=$objectClass)($searchField=$searchValue))"; + $ldapResult = @ldap_search($this->ldap, $searchBase, $filter, ['dn', $mailField, $nameField]); + if(!$ldapResult) { + $this->HandleLdapError("Fetch $objectClass"); + return []; + } + + $entries = @ldap_get_entries($this->ldap, $ldapResult); + if(!$entries) { + $this->HandleLdapError("Fetch $objectClass"); + return []; + } + + $results = []; + for($i = 0; $i < $entries["count"]; $i++) { + $entry = $entries[$i]; + + $result = new LdapResult(); + $result->dn = $entry["dn"]; + $result->name = $this->LdapGetAttribute($entry, $nameField, true, true); + $result->emails = $this->LdapGetAttribute($entry, $mailField, false, false); + + $results[] = $result; + } + + return $results; + } + + /** + * @param array $entry + * @param string $attribute + * @param bool $single + * @param bool $required + * @return string|string[] + */ + private function LdapGetAttribute(array $entry, string $attribute, bool $single = true, bool $required = false) { + if(!isset($entry[$attribute])) { + if($required) + $this->logger->Write("Attribute $attribute not found on object {$entry['dn']} while required", Type::NOTICE, self::LOG_KEY); + + return $single ? "" : []; + } + + if ($single) { + if($entry[$attribute]["count"] > 1) + $this->logger->Write("Attribute $attribute is multivalues while only a single value is expected", Type::NOTICE, self::LOG_KEY); + + return $entry[$attribute][0]; + } + + $result = $entry[$attribute]; + unset($result["count"]); + return array_values($result); + } +} + +class LdapResult { + /** @var string */ + public $dn; + + /** @var string */ + public $name; + + /** @var string[] */ + public $emails; +} \ No newline at end of file diff --git a/plugins/ldap-identities/VERSION b/plugins/ldap-identities/VERSION new file mode 100644 index 000000000..9f8e9b69a --- /dev/null +++ b/plugins/ldap-identities/VERSION @@ -0,0 +1 @@ +1.0 \ No newline at end of file diff --git a/plugins/ldap-identities/index.php b/plugins/ldap-identities/index.php new file mode 100644 index 000000000..580805ca1 --- /dev/null +++ b/plugins/ldap-identities/index.php @@ -0,0 +1,126 @@ +addHook("main.fabrica", 'MainFabrica'); + } + + public function MainFabrica(string $name, &$result) { + if($name !== 'identities') return; + + if(!is_array($result)) + $result = []; + + // Set up config + $config = LdapConfig::MakeConfig($this->Config()); + + $ldap = new LdapIdentities($config, $this->Manager()->Actions()->Logger()); + + $result[] = $ldap; + } + + protected function configMapping(): array + { + return [ + Property::NewInstance(LdapConfig::CONFIG_SERVER) + ->SetLabel("LDAP Server URL") + ->SetPlaceholder("ldap://server:port") + ->SetType(PluginPropertyType::STRING), + + Property::NewInstance(LdapConfig::CONFIG_PROTOCOL_VERSION) + ->SetLabel("LDAP Protocol Version") + ->SetType(PluginPropertyType::SELECTION) + ->SetDefaultValue([2, 3]), + + Property::NewInstance(LdapConfig::CONFIG_BIND_USER) + ->SetLabel("Bind User DN") + ->SetDescription("The user to use for binding to the LDAP server. Should be a DN or RDN. Leave empty for anonymous bind") + ->SetType(PluginPropertyType::STRING), + + Property::NewInstance(LdapConfig::CONFIG_BIND_PASSWORD) + ->SetLabel("Bind User Password") + ->SetDescription("Leave empty for anonymous bind") + ->SetType(PluginPropertyType::PASSWORD), + + Property::NewInstance(LdapConfig::CONFIG_USER_OBJECTCLASS) + ->SetLabel("User object class") + ->SetType(PluginPropertyType::STRING) + ->SetDefaultValue("user"), + + Property::NewInstance(LdapConfig::CONFIG_USER_FIELD_SEARCH) + ->SetLabel("User search field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the user object to search using the email the user logged in with") + ->SetDefaultValue("mail"), + + Property::NewInstance(LdapConfig::CONFIG_USER_FIELD_MAIL) + ->SetLabel("User mail field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the user object listing all identities (email addresses) of the user") + ->SetDefaultValue("mail"), + + Property::NewInstance(LdapConfig::CONFIG_USER_FIELD_NAME) + ->SetLabel("User name field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the user object with their default sender name") + ->SetDefaultValue("cn"), + + Property::NewInstance(LdapConfig::CONFIG_USER_BASE) + ->SetLabel("User base DN") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The base DN to search in for users"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_GET) + ->SetLabel("Find groups?") + ->SetType(PluginPropertyType::BOOL) + ->SetDescription("Whether or not to search for groups") + ->SetDefaultValue(true), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_OBJECTCLASS) + ->SetLabel("Group object class") + ->SetType(PluginPropertyType::STRING) + ->SetDefaultValue("group"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_FIELD_MAIL) + ->SetLabel("Group mail field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the group object listing all identities (email addresses) of the group") + ->SetDefaultValue("mail"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_FIELD_NAME) + ->SetLabel("Group name field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the group object with the name") + ->SetDefaultValue("cn"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_FIELD_MEMBER) + ->SetLabel("Group member field") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The field in the group object with all member DNs") + ->SetDefaultValue("member"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_SENDER_FORMAT) + ->SetLabel("Group mail sender format") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The sender name format for group addresses. Available template values: #USER# for the user name and #GROUP# for the group name") + ->SetDefaultValue("#USER# || #GROUP#"), + + Property::NewInstance(LdapConfig::CONFIG_GROUP_BASE) + ->SetLabel("Group base DN") + ->SetType(PluginPropertyType::STRING) + ->SetDescription("The base DN to search in for groups") + ]; + } +} \ No newline at end of file