You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1301 lines
37 KiB

LDAP User Cleanup: Port from stable7 without further adjustements LDAP User Cleanup background job for user clean up adjust user backend for clean up register background job remove dead code dependency injection make Helper non-static for proper testing check whether it is OK to run clean up job. Do not forget to pass arguments. use correct method to get the config from server methods can be private, proper indirect testing is given no automatic user deletion make limit readable for test purposes make method less complex add first tests let preferences accept limit and offset for getUsersForValue DI via constructor does not work for background jobs after detecting, now we have retrieving deleted users and their details we need this method to be public for now finalize export method, add missing getter clean up namespaces and get rid of unnecessary files helper is not static anymore cleanup according to scrutinizer add cli tool to show deleted users uses are necessary after recent namespace change also remove user from mappings table on deletion add occ command to delete users fix use statement improve output big fixes / improvements PHP doc return true in userExists early for cleaning up deleted users bump version control state and interval with one config.php setting, now ldapUserCleanupInterval. 0 will disable it. enabled by default. improve doc rename cli method to be consistent with others introduce ldapUserCleanupInterval in sample config don't show last login as unix epoche start when no login happend less log output consistent namespace for OfflineUser rename GarbageCollector to DeletedUsersIndex and move it to user subdir fix unit tests add tests for deleteUser more test adjustements Conflicts: apps/user_ldap/ajax/clearMappings.php apps/user_ldap/appinfo/app.php apps/user_ldap/lib/access.php apps/user_ldap/lib/helper.php apps/user_ldap/tests/helper.php core/register_command.php lib/private/preferences.php lib/private/user.php add ldap:check-user to check user existance on the fly Conflicts: apps/user_ldap/lib/helper.php forgotten file PHPdoc fixes, no code change and don't forget to adjust tests
11 years ago
12 years ago
  1. <?php
  2. /**
  3. * ownCloud LDAP Wizard
  4. *
  5. * @author Arthur Schiwon
  6. * @copyright 2013 Arthur Schiwon blizzz@owncloud.com
  7. *
  8. * This library is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  10. * License as published by the Free Software Foundation; either
  11. * version 3 of the License, or any later version.
  12. *
  13. * This library is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public
  19. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. namespace OCA\user_ldap\lib;
  23. class Wizard extends LDAPUtility {
  24. static protected $l;
  25. protected $access;
  26. protected $cr;
  27. protected $configuration;
  28. protected $result;
  29. protected $resultCache = array();
  30. const LRESULT_PROCESSED_OK = 2;
  31. const LRESULT_PROCESSED_INVALID = 3;
  32. const LRESULT_PROCESSED_SKIP = 4;
  33. const LFILTER_LOGIN = 2;
  34. const LFILTER_USER_LIST = 3;
  35. const LFILTER_GROUP_LIST = 4;
  36. const LFILTER_MODE_ASSISTED = 2;
  37. const LFILTER_MODE_RAW = 1;
  38. const LDAP_NW_TIMEOUT = 4;
  39. /**
  40. * Constructor
  41. * @param Configuration $configuration an instance of Configuration
  42. * @param ILDAPWrapper $ldap an instance of ILDAPWrapper
  43. */
  44. public function __construct(Configuration $configuration, ILDAPWrapper $ldap, Access $access) {
  45. parent::__construct($ldap);
  46. $this->configuration = $configuration;
  47. if(is_null(Wizard::$l)) {
  48. Wizard::$l = \OC::$server->getL10N('user_ldap');
  49. }
  50. $this->access = $access;
  51. $this->result = new WizardResult();
  52. }
  53. public function __destruct() {
  54. if($this->result->hasChanges()) {
  55. $this->configuration->saveConfiguration();
  56. }
  57. }
  58. /**
  59. * counts entries in the LDAP directory
  60. * @param string $filter the LDAP search filter
  61. * @param string $type a string being either 'users' or 'groups';
  62. * @return int|bool
  63. */
  64. public function countEntries($filter, $type) {
  65. $reqs = array('ldapHost', 'ldapPort', 'ldapBase');
  66. if($type === 'users') {
  67. $reqs[] = 'ldapUserFilter';
  68. }
  69. if(!$this->checkRequirements($reqs)) {
  70. throw new \Exception('Requirements not met', 400);
  71. }
  72. if($type === 'groups') {
  73. $result = $this->access->countGroups($filter);
  74. } else if($type === 'users') {
  75. $result = $this->access->countUsers($filter);
  76. } else {
  77. throw new \Exception('internal error: invald object type', 500);
  78. }
  79. return $result;
  80. }
  81. public function countGroups() {
  82. $filter = $this->configuration->ldapGroupFilter;
  83. if(empty($filter)) {
  84. $output = self::$l->n('%s group found', '%s groups found', 0, array(0));
  85. $this->result->addChange('ldap_group_count', $output);
  86. return $this->result;
  87. }
  88. try {
  89. $groupsTotal = $this->countEntries($filter, 'groups');
  90. } catch (\Exception $e) {
  91. //400 can be ignored, 500 is forwarded
  92. if($e->getCode() === 500) {
  93. throw $e;
  94. }
  95. return false;
  96. }
  97. $groupsTotal = ($groupsTotal !== false) ? $groupsTotal : 0;
  98. $output = self::$l->n('%s group found', '%s groups found', $groupsTotal, array($groupsTotal));
  99. $this->result->addChange('ldap_group_count', $output);
  100. return $this->result;
  101. }
  102. /**
  103. * @return WizardResult
  104. * @throws \Exception
  105. */
  106. public function countUsers() {
  107. $filter = $this->access->getFilterForUserCount();
  108. $usersTotal = $this->countEntries($filter, 'users');
  109. $usersTotal = ($usersTotal !== false) ? $usersTotal : 0;
  110. $output = self::$l->n('%s user found', '%s users found', $usersTotal, array($usersTotal));
  111. $this->result->addChange('ldap_user_count', $output);
  112. return $this->result;
  113. }
  114. /**
  115. * counts users with a specified attribute
  116. * @param string $attr
  117. * @param bool $existsCheck
  118. * @return int|bool
  119. */
  120. public function countUsersWithAttribute($attr, $existsCheck = false) {
  121. if(!$this->checkRequirements(array('ldapHost',
  122. 'ldapPort',
  123. 'ldapBase',
  124. 'ldapUserFilter',
  125. ))) {
  126. return false;
  127. }
  128. $filter = $this->access->combineFilterWithAnd(array(
  129. $this->configuration->ldapUserFilter,
  130. $attr . '=*'
  131. ));
  132. $limit = ($existsCheck === false) ? null : 1;
  133. return $this->access->countUsers($filter, array('dn'), $limit);
  134. }
  135. /**
  136. * detects the display name attribute. If a setting is already present that
  137. * returns at least one hit, the detection will be canceled.
  138. * @return WizardResult|bool
  139. * @throws \Exception
  140. */
  141. public function detectUserDisplayNameAttribute() {
  142. if(!$this->checkRequirements(array('ldapHost',
  143. 'ldapPort',
  144. 'ldapBase',
  145. 'ldapUserFilter',
  146. ))) {
  147. return false;
  148. }
  149. $attr = $this->configuration->ldapUserDisplayName;
  150. if($attr !== 'displayName' && !empty($attr)) {
  151. // most likely not the default value with upper case N,
  152. // verify it still produces a result
  153. $count = intval($this->countUsersWithAttribute($attr, true));
  154. if($count > 0) {
  155. //no change, but we sent it back to make sure the user interface
  156. //is still correct, even if the ajax call was cancelled inbetween
  157. $this->result->addChange('ldap_display_name', $attr);
  158. return $this->result;
  159. }
  160. }
  161. // first attribute that has at least one result wins
  162. $displayNameAttrs = array('displayname', 'cn');
  163. foreach ($displayNameAttrs as $attr) {
  164. $count = intval($this->countUsersWithAttribute($attr, true));
  165. if($count > 0) {
  166. $this->applyFind('ldap_display_name', $attr);
  167. return $this->result;
  168. }
  169. };
  170. throw new \Exception(self::$l->t('Could not detect user display name attribute. Please specify it yourself in advanced ldap settings.'));
  171. }
  172. /**
  173. * detects the most often used email attribute for users applying to the
  174. * user list filter. If a setting is already present that returns at least
  175. * one hit, the detection will be canceled.
  176. * @return WizardResult|bool
  177. */
  178. public function detectEmailAttribute() {
  179. if(!$this->checkRequirements(array('ldapHost',
  180. 'ldapPort',
  181. 'ldapBase',
  182. 'ldapUserFilter',
  183. ))) {
  184. return false;
  185. }
  186. $attr = $this->configuration->ldapEmailAttribute;
  187. if(!empty($attr)) {
  188. $count = intval($this->countUsersWithAttribute($attr, true));
  189. if($count > 0) {
  190. return false;
  191. }
  192. $writeLog = true;
  193. } else {
  194. $writeLog = false;
  195. }
  196. $emailAttributes = array('mail', 'mailPrimaryAddress');
  197. $winner = '';
  198. $maxUsers = 0;
  199. foreach($emailAttributes as $attr) {
  200. $count = $this->countUsersWithAttribute($attr);
  201. if($count > $maxUsers) {
  202. $maxUsers = $count;
  203. $winner = $attr;
  204. }
  205. }
  206. if($winner !== '') {
  207. $this->applyFind('ldap_email_attr', $winner);
  208. if($writeLog) {
  209. \OCP\Util::writeLog('user_ldap', 'The mail attribute has ' .
  210. 'automatically been reset, because the original value ' .
  211. 'did not return any results.', \OCP\Util::INFO);
  212. }
  213. }
  214. return $this->result;
  215. }
  216. /**
  217. * @return WizardResult
  218. * @throws \Exception
  219. */
  220. public function determineAttributes() {
  221. if(!$this->checkRequirements(array('ldapHost',
  222. 'ldapPort',
  223. 'ldapBase',
  224. 'ldapUserFilter',
  225. ))) {
  226. return false;
  227. }
  228. $attributes = $this->getUserAttributes();
  229. natcasesort($attributes);
  230. $attributes = array_values($attributes);
  231. $this->result->addOptions('ldap_loginfilter_attributes', $attributes);
  232. $selected = $this->configuration->ldapLoginFilterAttributes;
  233. if(is_array($selected) && !empty($selected)) {
  234. $this->result->addChange('ldap_loginfilter_attributes', $selected);
  235. }
  236. return $this->result;
  237. }
  238. /**
  239. * return the state of the Group Filter Mode
  240. * @return WizardResult
  241. */
  242. public function getGroupFilterMode() {
  243. $this->getFilterMode('ldapGroupFilterMode');
  244. return $this->result;
  245. }
  246. /**
  247. * return the state of the Login Filter Mode
  248. * @return WizardResult
  249. */
  250. public function getLoginFilterMode() {
  251. $this->getFilterMode('ldapLoginFilterMode');
  252. return $this->result;
  253. }
  254. /**
  255. * return the state of the User Filter Mode
  256. * @return WizardResult
  257. */
  258. public function getUserFilterMode() {
  259. $this->getFilterMode('ldapUserFilterMode');
  260. return $this->result;
  261. }
  262. /**
  263. * return the state of the mode of the specified filter
  264. * @param string $confKey contains the access key of the Configuration
  265. */
  266. private function getFilterMode($confKey) {
  267. $mode = $this->configuration->$confKey;
  268. if(is_null($mode)) {
  269. $mode = $this->LFILTER_MODE_ASSISTED;
  270. }
  271. $this->result->addChange($confKey, $mode);
  272. }
  273. /**
  274. * detects the available LDAP attributes
  275. * @return array|false The instance's WizardResult instance
  276. * @throws \Exception
  277. */
  278. private function getUserAttributes() {
  279. if(!$this->checkRequirements(array('ldapHost',
  280. 'ldapPort',
  281. 'ldapBase',
  282. 'ldapUserFilter',
  283. ))) {
  284. return false;
  285. }
  286. $cr = $this->getConnection();
  287. if(!$cr) {
  288. throw new \Exception('Could not connect to LDAP');
  289. }
  290. $base = $this->configuration->ldapBase[0];
  291. $filter = $this->configuration->ldapUserFilter;
  292. $rr = $this->ldap->search($cr, $base, $filter, array(), 1, 1);
  293. if(!$this->ldap->isResource($rr)) {
  294. return false;
  295. }
  296. $er = $this->ldap->firstEntry($cr, $rr);
  297. $attributes = $this->ldap->getAttributes($cr, $er);
  298. $pureAttributes = array();
  299. for($i = 0; $i < $attributes['count']; $i++) {
  300. $pureAttributes[] = $attributes[$i];
  301. }
  302. return $pureAttributes;
  303. }
  304. /**
  305. * detects the available LDAP groups
  306. * @return WizardResult|false the instance's WizardResult instance
  307. */
  308. public function determineGroupsForGroups() {
  309. return $this->determineGroups('ldap_groupfilter_groups',
  310. 'ldapGroupFilterGroups',
  311. false);
  312. }
  313. /**
  314. * detects the available LDAP groups
  315. * @return WizardResult|false the instance's WizardResult instance
  316. */
  317. public function determineGroupsForUsers() {
  318. return $this->determineGroups('ldap_userfilter_groups',
  319. 'ldapUserFilterGroups');
  320. }
  321. /**
  322. * detects the available LDAP groups
  323. * @param string $dbKey
  324. * @param string $confKey
  325. * @param bool $testMemberOf
  326. * @return WizardResult|false the instance's WizardResult instance
  327. * @throws \Exception
  328. */
  329. private function determineGroups($dbKey, $confKey, $testMemberOf = true) {
  330. if(!$this->checkRequirements(array('ldapHost',
  331. 'ldapPort',
  332. 'ldapBase',
  333. ))) {
  334. return false;
  335. }
  336. $cr = $this->getConnection();
  337. if(!$cr) {
  338. throw new \Exception('Could not connect to LDAP');
  339. }
  340. $groups = $this->fetchGroups($dbKey, $confKey);
  341. if($testMemberOf) {
  342. $this->configuration->hasMemberOfFilterSupport = $this->testMemberOf($groups);
  343. $this->result->markChange();
  344. if(!$this->configuration->hasMemberOfFilterSupport) {
  345. throw new \Exception('memberOf is not supported by the server');
  346. }
  347. }
  348. return $this->result;
  349. }
  350. /**
  351. * fetches all groups from LDAP
  352. * @param string $dbKey
  353. * @param string $confKey
  354. * @return array $groupEntries
  355. */
  356. public function fetchGroups($dbKey, $confKey) {
  357. $obclasses = array('posixGroup', 'group', 'zimbraDistributionList', 'groupOfNames');
  358. $filterParts = array();
  359. foreach($obclasses as $obclass) {
  360. $filterParts[] = 'objectclass='.$obclass;
  361. }
  362. //we filter for everything
  363. //- that looks like a group and
  364. //- has the group display name set
  365. $filter = $this->access->combineFilterWithOr($filterParts);
  366. $filter = $this->access->combineFilterWithAnd(array($filter, 'cn=*'));
  367. $groupNames = array();
  368. $groupEntries = array();
  369. $limit = 400;
  370. $offset = 0;
  371. do {
  372. // we need to request dn additionally here, otherwise memberOf
  373. // detection will fail later
  374. $result = $this->access->searchGroups($filter, array('cn', 'dn'), $limit, $offset);
  375. foreach($result as $item) {
  376. $groupNames[] = $item['cn'];
  377. $groupEntries[] = $item;
  378. }
  379. $offset += $limit;
  380. } while (count($groupNames) > 0 && count($groupNames) % $limit === 0);
  381. if(count($groupNames) > 0) {
  382. natsort($groupNames);
  383. $this->result->addOptions($dbKey, array_values($groupNames));
  384. } else {
  385. throw new \Exception(self::$l->t('Could not find the desired feature'));
  386. }
  387. $setFeatures = $this->configuration->$confKey;
  388. if(is_array($setFeatures) && !empty($setFeatures)) {
  389. //something is already configured? pre-select it.
  390. $this->result->addChange($dbKey, $setFeatures);
  391. }
  392. return $groupEntries;
  393. }
  394. public function determineGroupMemberAssoc() {
  395. if(!$this->checkRequirements(array('ldapHost',
  396. 'ldapPort',
  397. 'ldapGroupFilter',
  398. ))) {
  399. return false;
  400. }
  401. $attribute = $this->detectGroupMemberAssoc();
  402. if($attribute === false) {
  403. return false;
  404. }
  405. $this->configuration->setConfiguration(array('ldapGroupMemberAssocAttr' => $attribute));
  406. //so it will be saved on destruct
  407. $this->result->markChange();
  408. return $this->result;
  409. }
  410. /**
  411. * Detects the available object classes
  412. * @return WizardResult|false the instance's WizardResult instance
  413. * @throws \Exception
  414. */
  415. public function determineGroupObjectClasses() {
  416. if(!$this->checkRequirements(array('ldapHost',
  417. 'ldapPort',
  418. 'ldapBase',
  419. ))) {
  420. return false;
  421. }
  422. $cr = $this->getConnection();
  423. if(!$cr) {
  424. throw new \Exception('Could not connect to LDAP');
  425. }
  426. $obclasses = array('group', 'posixGroup', '*');
  427. $this->determineFeature($obclasses,
  428. 'objectclass',
  429. 'ldap_groupfilter_objectclass',
  430. 'ldapGroupFilterObjectclass',
  431. false);
  432. return $this->result;
  433. }
  434. /**
  435. * detects the available object classes
  436. * @return WizardResult
  437. * @throws \Exception
  438. */
  439. public function determineUserObjectClasses() {
  440. if(!$this->checkRequirements(array('ldapHost',
  441. 'ldapPort',
  442. 'ldapBase',
  443. ))) {
  444. return false;
  445. }
  446. $cr = $this->getConnection();
  447. if(!$cr) {
  448. throw new \Exception('Could not connect to LDAP');
  449. }
  450. $obclasses = array('inetOrgPerson', 'person', 'organizationalPerson',
  451. 'user', 'posixAccount', '*');
  452. $filter = $this->configuration->ldapUserFilter;
  453. //if filter is empty, it is probably the first time the wizard is called
  454. //then, apply suggestions.
  455. $this->determineFeature($obclasses,
  456. 'objectclass',
  457. 'ldap_userfilter_objectclass',
  458. 'ldapUserFilterObjectclass',
  459. empty($filter));
  460. return $this->result;
  461. }
  462. /**
  463. * @return WizardResult|false
  464. * @throws \Exception
  465. */
  466. public function getGroupFilter() {
  467. if(!$this->checkRequirements(array('ldapHost',
  468. 'ldapPort',
  469. 'ldapBase',
  470. ))) {
  471. return false;
  472. }
  473. //make sure the use display name is set
  474. $displayName = $this->configuration->ldapGroupDisplayName;
  475. if(empty($displayName)) {
  476. $d = $this->configuration->getDefaults();
  477. $this->applyFind('ldap_group_display_name',
  478. $d['ldap_group_display_name']);
  479. }
  480. $filter = $this->composeLdapFilter(self::LFILTER_GROUP_LIST);
  481. $this->applyFind('ldap_group_filter', $filter);
  482. return $this->result;
  483. }
  484. /**
  485. * @return WizardResult|false
  486. * @throws \Exception
  487. */
  488. public function getUserListFilter() {
  489. if(!$this->checkRequirements(array('ldapHost',
  490. 'ldapPort',
  491. 'ldapBase',
  492. ))) {
  493. return false;
  494. }
  495. //make sure the use display name is set
  496. $displayName = $this->configuration->ldapUserDisplayName;
  497. if(empty($displayName)) {
  498. $d = $this->configuration->getDefaults();
  499. $this->applyFind('ldap_display_name', $d['ldap_display_name']);
  500. }
  501. $filter = $this->composeLdapFilter(self::LFILTER_USER_LIST);
  502. if(!$filter) {
  503. throw new \Exception('Cannot create filter');
  504. }
  505. $this->applyFind('ldap_userlist_filter', $filter);
  506. return $this->result;
  507. }
  508. /**
  509. * @return bool|WizardResult
  510. * @throws \Exception
  511. */
  512. public function getUserLoginFilter() {
  513. if(!$this->checkRequirements(array('ldapHost',
  514. 'ldapPort',
  515. 'ldapBase',
  516. 'ldapUserFilter',
  517. ))) {
  518. return false;
  519. }
  520. $filter = $this->composeLdapFilter(self::LFILTER_LOGIN);
  521. if(!$filter) {
  522. throw new \Exception('Cannot create filter');
  523. }
  524. $this->applyFind('ldap_login_filter', $filter);
  525. return $this->result;
  526. }
  527. /**
  528. * Tries to determine the port, requires given Host, User DN and Password
  529. * @return WizardResult|false WizardResult on success, false otherwise
  530. * @throws \Exception
  531. */
  532. public function guessPortAndTLS() {
  533. if(!$this->checkRequirements(array('ldapHost',
  534. ))) {
  535. return false;
  536. }
  537. $this->checkHost();
  538. $portSettings = $this->getPortSettingsToTry();
  539. if(!is_array($portSettings)) {
  540. throw new \Exception(print_r($portSettings, true));
  541. }
  542. //proceed from the best configuration and return on first success
  543. foreach($portSettings as $setting) {
  544. $p = $setting['port'];
  545. $t = $setting['tls'];
  546. \OCP\Util::writeLog('user_ldap', 'Wiz: trying port '. $p . ', TLS '. $t, \OCP\Util::DEBUG);
  547. //connectAndBind may throw Exception, it needs to be catched by the
  548. //callee of this method
  549. if($this->connectAndBind($p, $t) === true) {
  550. $config = array('ldapPort' => $p,
  551. 'ldapTLS' => intval($t)
  552. );
  553. $this->configuration->setConfiguration($config);
  554. \OCP\Util::writeLog('user_ldap', 'Wiz: detected Port '. $p, \OCP\Util::DEBUG);
  555. $this->result->addChange('ldap_port', $p);
  556. return $this->result;
  557. }
  558. }
  559. //custom port, undetected (we do not brute force)
  560. return false;
  561. }
  562. /**
  563. * tries to determine a base dn from User DN or LDAP Host
  564. * @return WizardResult|false WizardResult on success, false otherwise
  565. */
  566. public function guessBaseDN() {
  567. if(!$this->checkRequirements(array('ldapHost',
  568. 'ldapPort',
  569. ))) {
  570. return false;
  571. }
  572. //check whether a DN is given in the agent name (99.9% of all cases)
  573. $base = null;
  574. $i = stripos($this->configuration->ldapAgentName, 'dc=');
  575. if($i !== false) {
  576. $base = substr($this->configuration->ldapAgentName, $i);
  577. if($this->testBaseDN($base)) {
  578. $this->applyFind('ldap_base', $base);
  579. return $this->result;
  580. }
  581. }
  582. //this did not help :(
  583. //Let's see whether we can parse the Host URL and convert the domain to
  584. //a base DN
  585. $helper = new Helper();
  586. $domain = $helper->getDomainFromURL($this->configuration->ldapHost);
  587. if(!$domain) {
  588. return false;
  589. }
  590. $dparts = explode('.', $domain);
  591. $base2 = implode('dc=', $dparts);
  592. if($base !== $base2 && $this->testBaseDN($base2)) {
  593. $this->applyFind('ldap_base', $base2);
  594. return $this->result;
  595. }
  596. return false;
  597. }
  598. /**
  599. * sets the found value for the configuration key in the WizardResult
  600. * as well as in the Configuration instance
  601. * @param string $key the configuration key
  602. * @param string $value the (detected) value
  603. *
  604. */
  605. private function applyFind($key, $value) {
  606. $this->result->addChange($key, $value);
  607. $this->configuration->setConfiguration(array($key => $value));
  608. }
  609. /**
  610. * Checks, whether a port was entered in the Host configuration
  611. * field. In this case the port will be stripped off, but also stored as
  612. * setting.
  613. */
  614. private function checkHost() {
  615. $host = $this->configuration->ldapHost;
  616. $hostInfo = parse_url($host);
  617. //removes Port from Host
  618. if(is_array($hostInfo) && isset($hostInfo['port'])) {
  619. $port = $hostInfo['port'];
  620. $host = str_replace(':'.$port, '', $host);
  621. $this->applyFind('ldap_host', $host);
  622. $this->applyFind('ldap_port', $port);
  623. }
  624. }
  625. /**
  626. * tries to detect the group member association attribute which is
  627. * one of 'uniqueMember', 'memberUid', 'member'
  628. * @return string|false, string with the attribute name, false on error
  629. * @throws \Exception
  630. */
  631. private function detectGroupMemberAssoc() {
  632. $possibleAttrs = array('uniqueMember', 'memberUid', 'member', 'unfugasdfasdfdfa');
  633. $filter = $this->configuration->ldapGroupFilter;
  634. if(empty($filter)) {
  635. return false;
  636. }
  637. $cr = $this->getConnection();
  638. if(!$cr) {
  639. throw new \Exception('Could not connect to LDAP');
  640. }
  641. $base = $this->configuration->ldapBase[0];
  642. $rr = $this->ldap->search($cr, $base, $filter, $possibleAttrs);
  643. if(!$this->ldap->isResource($rr)) {
  644. return false;
  645. }
  646. $er = $this->ldap->firstEntry($cr, $rr);
  647. while(is_resource($er)) {
  648. $this->ldap->getDN($cr, $er);
  649. $attrs = $this->ldap->getAttributes($cr, $er);
  650. $result = array();
  651. $possibleAttrsCount = count($possibleAttrs);
  652. for($i = 0; $i < $possibleAttrsCount; $i++) {
  653. if(isset($attrs[$possibleAttrs[$i]])) {
  654. $result[$possibleAttrs[$i]] = $attrs[$possibleAttrs[$i]]['count'];
  655. }
  656. }
  657. if(!empty($result)) {
  658. natsort($result);
  659. return key($result);
  660. }
  661. $er = $this->ldap->nextEntry($cr, $er);
  662. }
  663. return false;
  664. }
  665. /**
  666. * Checks whether for a given BaseDN results will be returned
  667. * @param string $base the BaseDN to test
  668. * @return bool true on success, false otherwise
  669. * @throws \Exception
  670. */
  671. private function testBaseDN($base) {
  672. $cr = $this->getConnection();
  673. if(!$cr) {
  674. throw new \Exception('Could not connect to LDAP');
  675. }
  676. //base is there, let's validate it. If we search for anything, we should
  677. //get a result set > 0 on a proper base
  678. $rr = $this->ldap->search($cr, $base, 'objectClass=*', array('dn'), 0, 1);
  679. if(!$this->ldap->isResource($rr)) {
  680. $errorNo = $this->ldap->errno($cr);
  681. $errorMsg = $this->ldap->error($cr);
  682. \OCP\Util::writeLog('user_ldap', 'Wiz: Could not search base '.$base.
  683. ' Error '.$errorNo.': '.$errorMsg, \OCP\Util::INFO);
  684. return false;
  685. }
  686. $entries = $this->ldap->countEntries($cr, $rr);
  687. return ($entries !== false) && ($entries > 0);
  688. }
  689. /**
  690. * Checks whether the server supports memberOf in LDAP Filter.
  691. * Requires that groups are determined, thus internally called from within
  692. * determineGroups()
  693. * @param array $groups
  694. * @return bool true if it does, false otherwise
  695. * @throws \Exception
  696. */
  697. private function testMemberOf($groups) {
  698. $cr = $this->getConnection();
  699. if(!$cr) {
  700. throw new \Exception('Could not connect to LDAP');
  701. }
  702. if(!is_array($this->configuration->ldapBase)
  703. || !isset($this->configuration->ldapBase[0])) {
  704. return false;
  705. }
  706. $base = $this->configuration->ldapBase[0];
  707. $filterPrefix = '(&(objectclass=*)(memberOf=';
  708. $filterSuffix = '))';
  709. foreach($groups as $groupProperties) {
  710. if(!isset($groupProperties['cn'])) {
  711. //assuming only groups have their cn cached :)
  712. continue;
  713. }
  714. $filter = strtolower($filterPrefix . $groupProperties['dn'] . $filterSuffix);
  715. $rr = $this->ldap->search($cr, $base, $filter, array('dn'));
  716. if(!$this->ldap->isResource($rr)) {
  717. continue;
  718. }
  719. $entries = $this->ldap->countEntries($cr, $rr);
  720. //we do not know which groups are empty, so test any and return
  721. //success on the first match that returns at least one user
  722. if(($entries !== false) && ($entries > 0)) {
  723. return true;
  724. }
  725. }
  726. return false;
  727. }
  728. /**
  729. * creates an LDAP Filter from given configuration
  730. * @param integer $filterType int, for which use case the filter shall be created
  731. * can be any of self::LFILTER_USER_LIST, self::LFILTER_LOGIN or
  732. * self::LFILTER_GROUP_LIST
  733. * @return string|false string with the filter on success, false otherwise
  734. * @throws \Exception
  735. */
  736. private function composeLdapFilter($filterType) {
  737. $filter = '';
  738. $parts = 0;
  739. switch ($filterType) {
  740. case self::LFILTER_USER_LIST:
  741. $objcs = $this->configuration->ldapUserFilterObjectclass;
  742. //glue objectclasses
  743. if(is_array($objcs) && count($objcs) > 0) {
  744. $filter .= '(|';
  745. foreach($objcs as $objc) {
  746. $filter .= '(objectclass=' . $objc . ')';
  747. }
  748. $filter .= ')';
  749. $parts++;
  750. }
  751. //glue group memberships
  752. if($this->configuration->hasMemberOfFilterSupport) {
  753. $cns = $this->configuration->ldapUserFilterGroups;
  754. if(is_array($cns) && count($cns) > 0) {
  755. $filter .= '(|';
  756. $cr = $this->getConnection();
  757. if(!$cr) {
  758. throw new \Exception('Could not connect to LDAP');
  759. }
  760. $base = $this->configuration->ldapBase[0];
  761. foreach($cns as $cn) {
  762. $rr = $this->ldap->search($cr, $base, 'cn=' . $cn, array('dn'));
  763. if(!$this->ldap->isResource($rr)) {
  764. continue;
  765. }
  766. $er = $this->ldap->firstEntry($cr, $rr);
  767. $dn = $this->ldap->getDN($cr, $er);
  768. $filter .= '(memberof=' . $dn . ')';
  769. }
  770. $filter .= ')';
  771. }
  772. $parts++;
  773. }
  774. //wrap parts in AND condition
  775. if($parts > 1) {
  776. $filter = '(&' . $filter . ')';
  777. }
  778. if(empty($filter)) {
  779. $filter = '(objectclass=*)';
  780. }
  781. break;
  782. case self::LFILTER_GROUP_LIST:
  783. $objcs = $this->configuration->ldapGroupFilterObjectclass;
  784. //glue objectclasses
  785. if(is_array($objcs) && count($objcs) > 0) {
  786. $filter .= '(|';
  787. foreach($objcs as $objc) {
  788. $filter .= '(objectclass=' . $objc . ')';
  789. }
  790. $filter .= ')';
  791. $parts++;
  792. }
  793. //glue group memberships
  794. $cns = $this->configuration->ldapGroupFilterGroups;
  795. if(is_array($cns) && count($cns) > 0) {
  796. $filter .= '(|';
  797. $base = $this->configuration->ldapBase[0];
  798. foreach($cns as $cn) {
  799. $filter .= '(cn=' . $cn . ')';
  800. }
  801. $filter .= ')';
  802. }
  803. $parts++;
  804. //wrap parts in AND condition
  805. if($parts > 1) {
  806. $filter = '(&' . $filter . ')';
  807. }
  808. break;
  809. case self::LFILTER_LOGIN:
  810. $ulf = $this->configuration->ldapUserFilter;
  811. $loginpart = '=%uid';
  812. $filterUsername = '';
  813. $userAttributes = $this->getUserAttributes();
  814. $userAttributes = array_change_key_case(array_flip($userAttributes));
  815. $parts = 0;
  816. $x = $this->configuration->ldapLoginFilterUsername;
  817. if($this->configuration->ldapLoginFilterUsername === '1') {
  818. $attr = '';
  819. if(isset($userAttributes['uid'])) {
  820. $attr = 'uid';
  821. } else if(isset($userAttributes['samaccountname'])) {
  822. $attr = 'samaccountname';
  823. } else if(isset($userAttributes['cn'])) {
  824. //fallback
  825. $attr = 'cn';
  826. }
  827. if(!empty($attr)) {
  828. $filterUsername = '(' . $attr . $loginpart . ')';
  829. $parts++;
  830. }
  831. }
  832. $filterEmail = '';
  833. if($this->configuration->ldapLoginFilterEmail === '1') {
  834. $filterEmail = '(|(mailPrimaryAddress=%uid)(mail=%uid))';
  835. $parts++;
  836. }
  837. $filterAttributes = '';
  838. $attrsToFilter = $this->configuration->ldapLoginFilterAttributes;
  839. if(is_array($attrsToFilter) && count($attrsToFilter) > 0) {
  840. $filterAttributes = '(|';
  841. foreach($attrsToFilter as $attribute) {
  842. $filterAttributes .= '(' . $attribute . $loginpart . ')';
  843. }
  844. $filterAttributes .= ')';
  845. $parts++;
  846. }
  847. $filterLogin = '';
  848. if($parts > 1) {
  849. $filterLogin = '(|';
  850. }
  851. $filterLogin .= $filterUsername;
  852. $filterLogin .= $filterEmail;
  853. $filterLogin .= $filterAttributes;
  854. if($parts > 1) {
  855. $filterLogin .= ')';
  856. }
  857. $filter = '(&'.$ulf.$filterLogin.')';
  858. break;
  859. }
  860. \OCP\Util::writeLog('user_ldap', 'Wiz: Final filter '.$filter, \OCP\Util::DEBUG);
  861. return $filter;
  862. }
  863. /**
  864. * Connects and Binds to an LDAP Server
  865. * @param int $port the port to connect with
  866. * @param bool $tls whether startTLS is to be used
  867. * @param bool $ncc
  868. * @return bool
  869. * @throws \Exception
  870. */
  871. private function connectAndBind($port = 389, $tls = false, $ncc = false) {
  872. if($ncc) {
  873. //No certificate check
  874. //FIXME: undo afterwards
  875. putenv('LDAPTLS_REQCERT=never');
  876. }
  877. //connect, does not really trigger any server communication
  878. \OCP\Util::writeLog('user_ldap', 'Wiz: Checking Host Info ', \OCP\Util::DEBUG);
  879. $host = $this->configuration->ldapHost;
  880. $hostInfo = parse_url($host);
  881. if(!$hostInfo) {
  882. throw new \Exception($this->l->t('Invalid Host'));
  883. }
  884. if(isset($hostInfo['scheme'])) {
  885. if(isset($hostInfo['port'])) {
  886. //problem
  887. } else {
  888. $host .= ':' . $port;
  889. }
  890. }
  891. \OCP\Util::writeLog('user_ldap', 'Wiz: Attempting to connect ', \OCP\Util::DEBUG);
  892. $cr = $this->ldap->connect($host, $port);
  893. if(!is_resource($cr)) {
  894. throw new \Exception($this->l->t('Invalid Host'));
  895. }
  896. \OCP\Util::writeLog('user_ldap', 'Wiz: Setting LDAP Options ', \OCP\Util::DEBUG);
  897. //set LDAP options
  898. $this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
  899. $this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
  900. $this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
  901. if($tls) {
  902. $isTlsWorking = @$this->ldap->startTls($cr);
  903. if(!$isTlsWorking) {
  904. return false;
  905. }
  906. }
  907. \OCP\Util::writeLog('user_ldap', 'Wiz: Attemping to Bind ', \OCP\Util::DEBUG);
  908. //interesting part: do the bind!
  909. $login = $this->ldap->bind($cr,
  910. $this->configuration->ldapAgentName,
  911. $this->configuration->ldapAgentPassword);
  912. if($login === true) {
  913. $this->ldap->unbind($cr);
  914. if($ncc) {
  915. throw new \Exception('Certificate cannot be validated.');
  916. }
  917. \OCP\Util::writeLog('user_ldap', 'Wiz: Bind successful to Port '. $port . ' TLS ' . intval($tls), \OCP\Util::DEBUG);
  918. return true;
  919. }
  920. $errNo = $this->ldap->errno($cr);
  921. $error = ldap_error($cr);
  922. $this->ldap->unbind($cr);
  923. if($errNo === -1 || ($errNo === 2 && $ncc)) {
  924. //host, port or TLS wrong
  925. return false;
  926. } else if ($errNo === 2) {
  927. return $this->connectAndBind($port, $tls, true);
  928. }
  929. throw new \Exception($error);
  930. }
  931. /**
  932. * checks whether a valid combination of agent and password has been
  933. * provided (either two values or nothing for anonymous connect)
  934. * @return bool, true if everything is fine, false otherwise
  935. */
  936. private function checkAgentRequirements() {
  937. $agent = $this->configuration->ldapAgentName;
  938. $pwd = $this->configuration->ldapAgentPassword;
  939. return ( (!empty($agent) && !empty($pwd))
  940. || (empty($agent) && empty($pwd)));
  941. }
  942. /**
  943. * @param array $reqs
  944. * @return bool
  945. */
  946. private function checkRequirements($reqs) {
  947. $this->checkAgentRequirements();
  948. foreach($reqs as $option) {
  949. $value = $this->configuration->$option;
  950. if(empty($value)) {
  951. return false;
  952. }
  953. }
  954. return true;
  955. }
  956. /**
  957. * does a cumulativeSearch on LDAP to get different values of a
  958. * specified attribute
  959. * @param string[] $filters array, the filters that shall be used in the search
  960. * @param string $attr the attribute of which a list of values shall be returned
  961. * @param int $dnReadLimit the amount of how many DNs should be analyzed.
  962. * The lower, the faster
  963. * @param string $maxF string. if not null, this variable will have the filter that
  964. * yields most result entries
  965. * @return array|false an array with the values on success, false otherwise
  966. */
  967. public function cumulativeSearchOnAttribute($filters, $attr, $dnReadLimit = 3, &$maxF = null) {
  968. $dnRead = array();
  969. $foundItems = array();
  970. $maxEntries = 0;
  971. if(!is_array($this->configuration->ldapBase)
  972. || !isset($this->configuration->ldapBase[0])) {
  973. return false;
  974. }
  975. $base = $this->configuration->ldapBase[0];
  976. $cr = $this->getConnection();
  977. if(!$this->ldap->isResource($cr)) {
  978. return false;
  979. }
  980. $lastFilter = null;
  981. if(isset($filters[count($filters)-1])) {
  982. $lastFilter = $filters[count($filters)-1];
  983. }
  984. foreach($filters as $filter) {
  985. if($lastFilter === $filter && count($foundItems) > 0) {
  986. //skip when the filter is a wildcard and results were found
  987. continue;
  988. }
  989. $rr = $this->ldap->search($cr, $base, $filter, array($attr));
  990. if(!$this->ldap->isResource($rr)) {
  991. continue;
  992. }
  993. $entries = $this->ldap->countEntries($cr, $rr);
  994. $getEntryFunc = 'firstEntry';
  995. if(($entries !== false) && ($entries > 0)) {
  996. if(!is_null($maxF) && $entries > $maxEntries) {
  997. $maxEntries = $entries;
  998. $maxF = $filter;
  999. }
  1000. $dnReadCount = 0;
  1001. do {
  1002. $entry = $this->ldap->$getEntryFunc($cr, $rr);
  1003. $getEntryFunc = 'nextEntry';
  1004. if(!$this->ldap->isResource($entry)) {
  1005. continue 2;
  1006. }
  1007. $rr = $entry; //will be expected by nextEntry next round
  1008. $attributes = $this->ldap->getAttributes($cr, $entry);
  1009. $dn = $this->ldap->getDN($cr, $entry);
  1010. if($dn === false || in_array($dn, $dnRead)) {
  1011. continue;
  1012. }
  1013. $newItems = array();
  1014. $state = $this->getAttributeValuesFromEntry($attributes,
  1015. $attr,
  1016. $newItems);
  1017. $dnReadCount++;
  1018. $foundItems = array_merge($foundItems, $newItems);
  1019. $this->resultCache[$dn][$attr] = $newItems;
  1020. $dnRead[] = $dn;
  1021. } while(($state === self::LRESULT_PROCESSED_SKIP
  1022. || $this->ldap->isResource($entry))
  1023. && ($dnReadLimit === 0 || $dnReadCount < $dnReadLimit));
  1024. }
  1025. }
  1026. return array_unique($foundItems);
  1027. }
  1028. /**
  1029. * determines if and which $attr are available on the LDAP server
  1030. * @param string[] $objectclasses the objectclasses to use as search filter
  1031. * @param string $attr the attribute to look for
  1032. * @param string $dbkey the dbkey of the setting the feature is connected to
  1033. * @param string $confkey the confkey counterpart for the $dbkey as used in the
  1034. * Configuration class
  1035. * @param bool $po whether the objectClass with most result entries
  1036. * shall be pre-selected via the result
  1037. * @return array|false list of found items.
  1038. * @throws \Exception
  1039. */
  1040. private function determineFeature($objectclasses, $attr, $dbkey, $confkey, $po = false) {
  1041. $cr = $this->getConnection();
  1042. if(!$cr) {
  1043. throw new \Exception('Could not connect to LDAP');
  1044. }
  1045. $p = 'objectclass=';
  1046. foreach($objectclasses as $key => $value) {
  1047. $objectclasses[$key] = $p.$value;
  1048. }
  1049. $maxEntryObjC = '';
  1050. //how deep to dig?
  1051. //When looking for objectclasses, testing few entries is sufficient,
  1052. $dig = 3;
  1053. $availableFeatures =
  1054. $this->cumulativeSearchOnAttribute($objectclasses, $attr,
  1055. $dig, $maxEntryObjC);
  1056. if(is_array($availableFeatures)
  1057. && count($availableFeatures) > 0) {
  1058. natcasesort($availableFeatures);
  1059. //natcasesort keeps indices, but we must get rid of them for proper
  1060. //sorting in the web UI. Therefore: array_values
  1061. $this->result->addOptions($dbkey, array_values($availableFeatures));
  1062. } else {
  1063. throw new \Exception(self::$l->t('Could not find the desired feature'));
  1064. }
  1065. $setFeatures = $this->configuration->$confkey;
  1066. if(is_array($setFeatures) && !empty($setFeatures)) {
  1067. //something is already configured? pre-select it.
  1068. $this->result->addChange($dbkey, $setFeatures);
  1069. } else if($po && !empty($maxEntryObjC)) {
  1070. //pre-select objectclass with most result entries
  1071. $maxEntryObjC = str_replace($p, '', $maxEntryObjC);
  1072. $this->applyFind($dbkey, $maxEntryObjC);
  1073. $this->result->addChange($dbkey, $maxEntryObjC);
  1074. }
  1075. return $availableFeatures;
  1076. }
  1077. /**
  1078. * appends a list of values fr
  1079. * @param resource $result the return value from ldap_get_attributes
  1080. * @param string $attribute the attribute values to look for
  1081. * @param array &$known new values will be appended here
  1082. * @return int, state on of the class constants LRESULT_PROCESSED_OK,
  1083. * LRESULT_PROCESSED_INVALID or LRESULT_PROCESSED_SKIP
  1084. */
  1085. private function getAttributeValuesFromEntry($result, $attribute, &$known) {
  1086. if(!is_array($result)
  1087. || !isset($result['count'])
  1088. || !$result['count'] > 0) {
  1089. return self::LRESULT_PROCESSED_INVALID;
  1090. }
  1091. // strtolower on all keys for proper comparison
  1092. $result = \OCP\Util::mb_array_change_key_case($result);
  1093. $attribute = strtolower($attribute);
  1094. if(isset($result[$attribute])) {
  1095. foreach($result[$attribute] as $key => $val) {
  1096. if($key === 'count') {
  1097. continue;
  1098. }
  1099. if(!in_array($val, $known)) {
  1100. $known[] = $val;
  1101. }
  1102. }
  1103. return self::LRESULT_PROCESSED_OK;
  1104. } else {
  1105. return self::LRESULT_PROCESSED_SKIP;
  1106. }
  1107. }
  1108. /**
  1109. * @return bool|mixed
  1110. */
  1111. private function getConnection() {
  1112. if(!is_null($this->cr)) {
  1113. return $this->cr;
  1114. }
  1115. $cr = $this->ldap->connect(
  1116. $this->configuration->ldapHost.':'.$this->configuration->ldapPort,
  1117. $this->configuration->ldapPort);
  1118. $this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
  1119. $this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
  1120. $this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
  1121. if($this->configuration->ldapTLS === 1) {
  1122. $this->ldap->startTls($cr);
  1123. }
  1124. $lo = @$this->ldap->bind($cr,
  1125. $this->configuration->ldapAgentName,
  1126. $this->configuration->ldapAgentPassword);
  1127. if($lo === true) {
  1128. $this->$cr = $cr;
  1129. return $cr;
  1130. }
  1131. return false;
  1132. }
  1133. /**
  1134. * @return array
  1135. */
  1136. private function getDefaultLdapPortSettings() {
  1137. static $settings = array(
  1138. array('port' => 7636, 'tls' => false),
  1139. array('port' => 636, 'tls' => false),
  1140. array('port' => 7389, 'tls' => true),
  1141. array('port' => 389, 'tls' => true),
  1142. array('port' => 7389, 'tls' => false),
  1143. array('port' => 389, 'tls' => false),
  1144. );
  1145. return $settings;
  1146. }
  1147. /**
  1148. * @return array
  1149. */
  1150. private function getPortSettingsToTry() {
  1151. //389 ← LDAP / Unencrypted or StartTLS
  1152. //636 ← LDAPS / SSL
  1153. //7xxx ← UCS. need to be checked first, because both ports may be open
  1154. $host = $this->configuration->ldapHost;
  1155. $port = intval($this->configuration->ldapPort);
  1156. $portSettings = array();
  1157. //In case the port is already provided, we will check this first
  1158. if($port > 0) {
  1159. $hostInfo = parse_url($host);
  1160. if(!(is_array($hostInfo)
  1161. && isset($hostInfo['scheme'])
  1162. && stripos($hostInfo['scheme'], 'ldaps') !== false)) {
  1163. $portSettings[] = array('port' => $port, 'tls' => true);
  1164. }
  1165. $portSettings[] =array('port' => $port, 'tls' => false);
  1166. }
  1167. //default ports
  1168. $portSettings = array_merge($portSettings,
  1169. $this->getDefaultLdapPortSettings());
  1170. return $portSettings;
  1171. }
  1172. }