Helper.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\User_LDAP;
  8. use OCP\Cache\CappedMemoryCache;
  9. use OCP\DB\QueryBuilder\IQueryBuilder;
  10. use OCP\IConfig;
  11. use OCP\IDBConnection;
  12. class Helper {
  13. /** @var CappedMemoryCache<string> */
  14. protected CappedMemoryCache $sanitizeDnCache;
  15. public function __construct(
  16. private IConfig $config,
  17. private IDBConnection $connection,
  18. ) {
  19. $this->sanitizeDnCache = new CappedMemoryCache(10000);
  20. }
  21. /**
  22. * returns prefixes for each saved LDAP/AD server configuration.
  23. *
  24. * @param bool $activeConfigurations optional, whether only active configuration shall be
  25. * retrieved, defaults to false
  26. * @return array with a list of the available prefixes
  27. *
  28. * Configuration prefixes are used to set up configurations for n LDAP or
  29. * AD servers. Since configuration is stored in the database, table
  30. * appconfig under appid user_ldap, the common identifiers in column
  31. * 'configkey' have a prefix. The prefix for the very first server
  32. * configuration is empty.
  33. * Configkey Examples:
  34. * Server 1: ldap_login_filter
  35. * Server 2: s1_ldap_login_filter
  36. * Server 3: s2_ldap_login_filter
  37. *
  38. * The prefix needs to be passed to the constructor of Connection class,
  39. * except the default (first) server shall be connected to.
  40. *
  41. */
  42. public function getServerConfigurationPrefixes($activeConfigurations = false): array {
  43. $referenceConfigkey = 'ldap_configuration_active';
  44. $keys = $this->getServersConfig($referenceConfigkey);
  45. $prefixes = [];
  46. foreach ($keys as $key) {
  47. if ($activeConfigurations && $this->config->getAppValue('user_ldap', $key, '0') !== '1') {
  48. continue;
  49. }
  50. $len = strlen($key) - strlen($referenceConfigkey);
  51. $prefixes[] = substr($key, 0, $len);
  52. }
  53. asort($prefixes);
  54. return $prefixes;
  55. }
  56. /**
  57. *
  58. * determines the host for every configured connection
  59. *
  60. * @return array an array with configprefix as keys
  61. *
  62. */
  63. public function getServerConfigurationHosts() {
  64. $referenceConfigkey = 'ldap_host';
  65. $keys = $this->getServersConfig($referenceConfigkey);
  66. $result = [];
  67. foreach ($keys as $key) {
  68. $len = strlen($key) - strlen($referenceConfigkey);
  69. $prefix = substr($key, 0, $len);
  70. $result[$prefix] = $this->config->getAppValue('user_ldap', $key);
  71. }
  72. return $result;
  73. }
  74. /**
  75. * return the next available configuration prefix
  76. *
  77. * @return string
  78. */
  79. public function getNextServerConfigurationPrefix() {
  80. $serverConnections = $this->getServerConfigurationPrefixes();
  81. if (count($serverConnections) === 0) {
  82. return 's01';
  83. }
  84. sort($serverConnections);
  85. $lastKey = array_pop($serverConnections);
  86. $lastNumber = (int)str_replace('s', '', $lastKey);
  87. return 's' . str_pad((string)($lastNumber + 1), 2, '0', STR_PAD_LEFT);
  88. }
  89. private function getServersConfig(string $value): array {
  90. $regex = '/' . $value . '$/S';
  91. $keys = $this->config->getAppKeys('user_ldap');
  92. $result = [];
  93. foreach ($keys as $key) {
  94. if (preg_match($regex, $key) === 1) {
  95. $result[] = $key;
  96. }
  97. }
  98. return $result;
  99. }
  100. /**
  101. * deletes a given saved LDAP/AD server configuration.
  102. *
  103. * @param string $prefix the configuration prefix of the config to delete
  104. * @return bool true on success, false otherwise
  105. */
  106. public function deleteServerConfiguration($prefix) {
  107. if (!in_array($prefix, self::getServerConfigurationPrefixes())) {
  108. return false;
  109. }
  110. $query = $this->connection->getQueryBuilder();
  111. $query->delete('appconfig')
  112. ->where($query->expr()->eq('appid', $query->createNamedParameter('user_ldap')))
  113. ->andWhere($query->expr()->like('configkey', $query->createNamedParameter((string)$prefix . '%')))
  114. ->andWhere($query->expr()->notIn('configkey', $query->createNamedParameter([
  115. 'enabled',
  116. 'installed_version',
  117. 'types',
  118. 'bgjUpdateGroupsLastRun',
  119. ], IQueryBuilder::PARAM_STR_ARRAY)));
  120. if (empty($prefix)) {
  121. $query->andWhere($query->expr()->notLike('configkey', $query->createNamedParameter('s%')));
  122. }
  123. $deletedRows = $query->execute();
  124. return $deletedRows !== 0;
  125. }
  126. /**
  127. * checks whether there is one or more disabled LDAP configurations
  128. */
  129. public function haveDisabledConfigurations(): bool {
  130. $all = $this->getServerConfigurationPrefixes(false);
  131. $active = $this->getServerConfigurationPrefixes(true);
  132. return count($all) !== count($active) || count($all) === 0;
  133. }
  134. /**
  135. * extracts the domain from a given URL
  136. *
  137. * @param string $url the URL
  138. * @return string|false domain as string on success, false otherwise
  139. */
  140. public function getDomainFromURL($url) {
  141. $uinfo = parse_url($url);
  142. if (!is_array($uinfo)) {
  143. return false;
  144. }
  145. $domain = false;
  146. if (isset($uinfo['host'])) {
  147. $domain = $uinfo['host'];
  148. } elseif (isset($uinfo['path'])) {
  149. $domain = $uinfo['path'];
  150. }
  151. return $domain;
  152. }
  153. /**
  154. * sanitizes a DN received from the LDAP server
  155. *
  156. * This is used and done to have a stable format of DNs that can be compared
  157. * and identified again. The input DN value is modified as following:
  158. *
  159. * 1) whitespaces after commas are removed
  160. * 2) the DN is turned to lower-case
  161. * 3) the DN is escaped according to RFC 2253
  162. *
  163. * When a future DN is supposed to be used as a base parameter, it has to be
  164. * run through DNasBaseParameter() first, to recode \5c into a backslash
  165. * again, otherwise the search or read operation will fail with LDAP error
  166. * 32, NO_SUCH_OBJECT. Regular usage in LDAP filters requires the backslash
  167. * being escaped, however.
  168. *
  169. * Internally, DNs are stored in their sanitized form.
  170. *
  171. * @param array|string $dn the DN in question
  172. * @return array|string the sanitized DN
  173. */
  174. public function sanitizeDN($dn) {
  175. //treating multiple base DNs
  176. if (is_array($dn)) {
  177. $result = [];
  178. foreach ($dn as $singleDN) {
  179. $result[] = $this->sanitizeDN($singleDN);
  180. }
  181. return $result;
  182. }
  183. if (!is_string($dn)) {
  184. throw new \LogicException('String expected ' . \gettype($dn) . ' given');
  185. }
  186. if (($sanitizedDn = $this->sanitizeDnCache->get($dn)) !== null) {
  187. return $sanitizedDn;
  188. }
  189. //OID sometimes gives back DNs with whitespace after the comma
  190. // a la "uid=foo, cn=bar, dn=..." We need to tackle this!
  191. $sanitizedDn = preg_replace('/([^\\\]),(\s+)/u', '\1,', $dn);
  192. //make comparisons and everything work
  193. $sanitizedDn = mb_strtolower($sanitizedDn, 'UTF-8');
  194. //escape DN values according to RFC 2253 – this is already done by ldap_explode_dn
  195. //to use the DN in search filters, \ needs to be escaped to \5c additionally
  196. //to use them in bases, we convert them back to simple backslashes in readAttribute()
  197. $replacements = [
  198. '\,' => '\5c2C',
  199. '\=' => '\5c3D',
  200. '\+' => '\5c2B',
  201. '\<' => '\5c3C',
  202. '\>' => '\5c3E',
  203. '\;' => '\5c3B',
  204. '\"' => '\5c22',
  205. '\#' => '\5c23',
  206. '(' => '\28',
  207. ')' => '\29',
  208. '*' => '\2A',
  209. ];
  210. $sanitizedDn = str_replace(array_keys($replacements), array_values($replacements), $sanitizedDn);
  211. $this->sanitizeDnCache->set($dn, $sanitizedDn);
  212. return $sanitizedDn;
  213. }
  214. /**
  215. * converts a stored DN so it can be used as base parameter for LDAP queries, internally we store them for usage in LDAP filters
  216. *
  217. * @param string $dn the DN
  218. * @return string
  219. */
  220. public function DNasBaseParameter($dn) {
  221. return str_ireplace('\\5c', '\\', $dn);
  222. }
  223. /**
  224. * listens to a hook thrown by server2server sharing and replaces the given
  225. * login name by a username, if it matches an LDAP user.
  226. *
  227. * @param array $param contains a reference to a $uid var under 'uid' key
  228. * @throws \Exception
  229. */
  230. public static function loginName2UserName($param): void {
  231. if (!isset($param['uid'])) {
  232. throw new \Exception('key uid is expected to be set in $param');
  233. }
  234. $userBackend = \OC::$server->get(User_Proxy::class);
  235. $uid = $userBackend->loginName2UserName($param['uid']);
  236. if ($uid !== false) {
  237. $param['uid'] = $uid;
  238. }
  239. }
  240. }