wizard.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065
  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 $cr;
  26. protected $configuration;
  27. protected $result;
  28. protected $resultCache = array();
  29. const LRESULT_PROCESSED_OK = 2;
  30. const LRESULT_PROCESSED_INVALID = 3;
  31. const LRESULT_PROCESSED_SKIP = 4;
  32. const LFILTER_LOGIN = 2;
  33. const LFILTER_USER_LIST = 3;
  34. const LFILTER_GROUP_LIST = 4;
  35. const LFILTER_MODE_ASSISTED = 2;
  36. const LFILTER_MODE_RAW = 1;
  37. const LDAP_NW_TIMEOUT = 4;
  38. /**
  39. * @brief Constructor
  40. * @param $configuration an instance of Configuration
  41. * @param $ldap an instance of ILDAPWrapper
  42. */
  43. public function __construct(Configuration $configuration, ILDAPWrapper $ldap) {
  44. parent::__construct($ldap);
  45. $this->configuration = $configuration;
  46. if(is_null(Wizard::$l)) {
  47. Wizard::$l = \OC_L10N::get('user_ldap');
  48. }
  49. $this->result = new WizardResult;
  50. }
  51. public function __destruct() {
  52. if($this->result->hasChanges()) {
  53. $this->configuration->saveConfiguration();
  54. }
  55. }
  56. public function countGroups() {
  57. if(!$this->checkRequirements(array('ldapHost',
  58. 'ldapPort',
  59. 'ldapBase',
  60. ))) {
  61. return false;
  62. }
  63. $base = $this->configuration->ldapBase[0];
  64. $filter = $this->configuration->ldapGroupFilter;
  65. \OCP\Util::writeLog('user_ldap', 'Wiz: g filter '. print_r($filter, true), \OCP\Util::DEBUG);
  66. $l = \OC_L10N::get('user_ldap');
  67. if(empty($filter)) {
  68. $output = $l->n('%s group found', '%s groups found', 0, array(0));
  69. $this->result->addChange('ldap_group_count', $output);
  70. return $this->result;
  71. }
  72. $cr = $this->getConnection();
  73. if(!$cr) {
  74. throw new \Exception('Could not connect to LDAP');
  75. }
  76. $rr = $this->ldap->search($cr, $base, $filter, array('dn'));
  77. if(!$this->ldap->isResource($rr)) {
  78. return false;
  79. }
  80. $entries = $this->ldap->countEntries($cr, $rr);
  81. $entries = ($entries !== false) ? $entries : 0;
  82. $output = $l->n('%s group found', '%s groups found', $entries, $entries);
  83. $this->result->addChange('ldap_group_count', $output);
  84. return $this->result;
  85. }
  86. public function countUsers() {
  87. if(!$this->checkRequirements(array('ldapHost',
  88. 'ldapPort',
  89. 'ldapBase',
  90. 'ldapUserFilter',
  91. ))) {
  92. return false;
  93. }
  94. $cr = $this->getConnection();
  95. if(!$cr) {
  96. throw new \Exception('Could not connect to LDAP');
  97. }
  98. $base = $this->configuration->ldapBase[0];
  99. $filter = $this->configuration->ldapUserFilter;
  100. $rr = $this->ldap->search($cr, $base, $filter, array('dn'));
  101. if(!$this->ldap->isResource($rr)) {
  102. return false;
  103. }
  104. $entries = $this->ldap->countEntries($cr, $rr);
  105. $entries = ($entries !== false) ? $entries : 0;
  106. $l = \OC_L10N::get('user_ldap');
  107. $output = $l->n('%s user found', '%s users found', $entries, $entries);
  108. $this->result->addChange('ldap_user_count', $output);
  109. return $this->result;
  110. }
  111. public function determineAttributes() {
  112. if(!$this->checkRequirements(array('ldapHost',
  113. 'ldapPort',
  114. 'ldapBase',
  115. 'ldapUserFilter',
  116. ))) {
  117. return false;
  118. }
  119. $attributes = $this->getUserAttributes();
  120. natcasesort($attributes);
  121. $attributes = array_values($attributes);
  122. $this->result->addOptions('ldap_loginfilter_attributes', $attributes);
  123. $selected = $this->configuration->ldapLoginFilterAttributes;
  124. if(is_array($selected) && !empty($selected)) {
  125. $this->result->addChange('ldap_loginfilter_attributes', $selected);
  126. }
  127. return $this->result;
  128. }
  129. /**
  130. * @brief return the state of the Group Filter Mode
  131. */
  132. public function getGroupFilterMode() {
  133. $this->getFilterMode('ldapGroupFilterMode');
  134. return $this->result;
  135. }
  136. /**
  137. * @brief return the state of the Login Filter Mode
  138. */
  139. public function getLoginFilterMode() {
  140. $this->getFilterMode('ldapLoginFilterMode');
  141. return $this->result;
  142. }
  143. /**
  144. * @brief return the state of the User Filter Mode
  145. */
  146. public function getUserFilterMode() {
  147. $this->getFilterMode('ldapUserFilterMode');
  148. return $this->result;
  149. }
  150. /**
  151. * @brief return the state of the mode of the specified filter
  152. * @param $confkey string, contains the access key of the Configuration
  153. */
  154. private function getFilterMode($confkey) {
  155. $mode = $this->configuration->$confkey;
  156. if(is_null($mode)) {
  157. $mode = $this->LFILTER_MODE_ASSISTED;
  158. }
  159. $this->result->addChange($confkey, $mode);
  160. }
  161. /**
  162. * @brief detects the available LDAP attributes
  163. * @returns the instance's WizardResult instance
  164. */
  165. private function getUserAttributes() {
  166. if(!$this->checkRequirements(array('ldapHost',
  167. 'ldapPort',
  168. 'ldapBase',
  169. 'ldapUserFilter',
  170. ))) {
  171. return false;
  172. }
  173. $cr = $this->getConnection();
  174. if(!$cr) {
  175. throw new \Exception('Could not connect to LDAP');
  176. }
  177. $base = $this->configuration->ldapBase[0];
  178. $filter = $this->configuration->ldapUserFilter;
  179. $rr = $this->ldap->search($cr, $base, $filter, array(), 1, 1);
  180. if(!$this->ldap->isResource($rr)) {
  181. return false;
  182. }
  183. $er = $this->ldap->firstEntry($cr, $rr);
  184. $attributes = $this->ldap->getAttributes($cr, $er);
  185. $pureAttributes = array();
  186. for($i = 0; $i < $attributes['count']; $i++) {
  187. $pureAttributes[] = $attributes[$i];
  188. }
  189. return $pureAttributes;
  190. }
  191. /**
  192. * @brief detects the available LDAP groups
  193. * @returns the instance's WizardResult instance
  194. */
  195. public function determineGroupsForGroups() {
  196. return $this->determineGroups('ldap_groupfilter_groups',
  197. 'ldapGroupFilterGroups',
  198. false);
  199. }
  200. /**
  201. * @brief detects the available LDAP groups
  202. * @returns the instance's WizardResult instance
  203. */
  204. public function determineGroupsForUsers() {
  205. return $this->determineGroups('ldap_userfilter_groups',
  206. 'ldapUserFilterGroups');
  207. }
  208. /**
  209. * @brief detects the available LDAP groups
  210. * @returns the instance's WizardResult instance
  211. */
  212. private function determineGroups($dbkey, $confkey, $testMemberOf = true) {
  213. if(!$this->checkRequirements(array('ldapHost',
  214. 'ldapPort',
  215. 'ldapBase',
  216. ))) {
  217. return false;
  218. }
  219. $cr = $this->getConnection();
  220. if(!$cr) {
  221. throw new \Exception('Could not connect to LDAP');
  222. }
  223. $obclasses = array('posixGroup', 'group', 'zimbraDistributionList', '*');
  224. $this->determineFeature($obclasses, 'cn', $dbkey, $confkey);
  225. if($testMemberOf) {
  226. $this->configuration->hasMemberOfFilterSupport = $this->testMemberOf();
  227. $this->result->markChange();
  228. if(!$this->configuration->hasMemberOfFilterSupport) {
  229. throw new \Exception('memberOf is not supported by the server');
  230. }
  231. }
  232. return $this->result;
  233. }
  234. public function determineGroupMemberAssoc() {
  235. if(!$this->checkRequirements(array('ldapHost',
  236. 'ldapPort',
  237. 'ldapGroupFilter',
  238. ))) {
  239. return false;
  240. }
  241. $attribute = $this->detectGroupMemberAssoc();
  242. if($attribute === false) {
  243. return false;
  244. }
  245. $this->configuration->setConfiguration(array('ldapGroupMemberAssocAttr' => $attribute));
  246. //so it will be saved on destruct
  247. $this->result->markChange();
  248. return $this->result;
  249. }
  250. /**
  251. * @brief detects the available object classes
  252. * @returns the instance's WizardResult instance
  253. */
  254. public function determineGroupObjectClasses() {
  255. if(!$this->checkRequirements(array('ldapHost',
  256. 'ldapPort',
  257. 'ldapBase',
  258. ))) {
  259. return false;
  260. }
  261. $cr = $this->getConnection();
  262. if(!$cr) {
  263. throw new \Exception('Could not connect to LDAP');
  264. }
  265. $obclasses = array('group', 'posixGroup', '*');
  266. $this->determineFeature($obclasses,
  267. 'objectclass',
  268. 'ldap_groupfilter_objectclass',
  269. 'ldapGroupFilterObjectclass',
  270. false);
  271. return $this->result;
  272. }
  273. /**
  274. * @brief detects the available object classes
  275. * @returns the instance's WizardResult instance
  276. */
  277. public function determineUserObjectClasses() {
  278. if(!$this->checkRequirements(array('ldapHost',
  279. 'ldapPort',
  280. 'ldapBase',
  281. ))) {
  282. return false;
  283. }
  284. $cr = $this->getConnection();
  285. if(!$cr) {
  286. throw new \Exception('Could not connect to LDAP');
  287. }
  288. $obclasses = array('inetOrgPerson', 'person', 'organizationalPerson',
  289. 'user', 'posixAccount', '*');
  290. $filter = $this->configuration->ldapUserFilter;
  291. //if filter is empty, it is probably the first time the wizard is called
  292. //then, apply suggestions.
  293. $this->determineFeature($obclasses,
  294. 'objectclass',
  295. 'ldap_userfilter_objectclass',
  296. 'ldapUserFilterObjectclass',
  297. empty($filter));
  298. return $this->result;
  299. }
  300. public function getGroupFilter() {
  301. if(!$this->checkRequirements(array('ldapHost',
  302. 'ldapPort',
  303. 'ldapBase',
  304. ))) {
  305. return false;
  306. }
  307. //make sure the use display name is set
  308. $displayName = $this->configuration->ldapGroupDisplayName;
  309. if(empty($displayName)) {
  310. $d = $this->configuration->getDefaults();
  311. $this->applyFind('ldap_group_display_name',
  312. $d['ldap_group_display_name']);
  313. }
  314. $filter = $this->composeLdapFilter(self::LFILTER_GROUP_LIST);
  315. $this->applyFind('ldap_group_filter', $filter);
  316. return $this->result;
  317. }
  318. public function getUserListFilter() {
  319. if(!$this->checkRequirements(array('ldapHost',
  320. 'ldapPort',
  321. 'ldapBase',
  322. ))) {
  323. return false;
  324. }
  325. //make sure the use display name is set
  326. $displayName = $this->configuration->ldapUserDisplayName;
  327. if(empty($displayName)) {
  328. $d = $this->configuration->getDefaults();
  329. $this->applyFind('ldap_display_name', $d['ldap_display_name']);
  330. }
  331. $filter = $this->composeLdapFilter(self::LFILTER_USER_LIST);
  332. if(!$filter) {
  333. throw new \Exception('Cannot create filter');
  334. }
  335. $this->applyFind('ldap_userlist_filter', $filter);
  336. return $this->result;
  337. }
  338. public function getUserLoginFilter() {
  339. if(!$this->checkRequirements(array('ldapHost',
  340. 'ldapPort',
  341. 'ldapBase',
  342. 'ldapUserFilter',
  343. ))) {
  344. return false;
  345. }
  346. $filter = $this->composeLdapFilter(self::LFILTER_LOGIN);
  347. if(!$filter) {
  348. throw new \Exception('Cannot create filter');
  349. }
  350. $this->applyFind('ldap_login_filter', $filter);
  351. return $this->result;
  352. }
  353. /**
  354. * Tries to determine the port, requires given Host, User DN and Password
  355. * @returns mixed WizardResult on success, false otherwise
  356. */
  357. public function guessPortAndTLS() {
  358. if(!$this->checkRequirements(array('ldapHost',
  359. ))) {
  360. return false;
  361. }
  362. $this->checkHost();
  363. $portSettings = $this->getPortSettingsToTry();
  364. if(!is_array($portSettings)) {
  365. throw new \Exception(print_r($portSettings, true));
  366. }
  367. //proceed from the best configuration and return on first success
  368. foreach($portSettings as $setting) {
  369. $p = $setting['port'];
  370. $t = $setting['tls'];
  371. \OCP\Util::writeLog('user_ldap', 'Wiz: trying port '. $p . ', TLS '. $t, \OCP\Util::DEBUG);
  372. //connectAndBind may throw Exception, it needs to be catched by the
  373. //callee of this method
  374. if($this->connectAndBind($p, $t) === true) {
  375. $config = array('ldapPort' => $p,
  376. 'ldapTLS' => intval($t)
  377. );
  378. $this->configuration->setConfiguration($config);
  379. \OCP\Util::writeLog('user_ldap', 'Wiz: detected Port '. $p, \OCP\Util::DEBUG);
  380. $this->result->addChange('ldap_port', $p);
  381. return $this->result;
  382. }
  383. }
  384. //custom port, undetected (we do not brute force)
  385. return false;
  386. }
  387. /**
  388. * @brief tries to determine a base dn from User DN or LDAP Host
  389. * @returns mixed WizardResult on success, false otherwise
  390. */
  391. public function guessBaseDN() {
  392. if(!$this->checkRequirements(array('ldapHost',
  393. 'ldapPort',
  394. ))) {
  395. return false;
  396. }
  397. //check whether a DN is given in the agent name (99.9% of all cases)
  398. $base = null;
  399. $i = stripos($this->configuration->ldapAgentName, 'dc=');
  400. if($i !== false) {
  401. $base = substr($this->configuration->ldapAgentName, $i);
  402. if($this->testBaseDN($base)) {
  403. $this->applyFind('ldap_base', $base);
  404. return $this->result;
  405. }
  406. }
  407. //this did not help :(
  408. //Let's see whether we can parse the Host URL and convert the domain to
  409. //a base DN
  410. $domain = Helper::getDomainFromURL($this->configuration->ldapHost);
  411. if(!$domain) {
  412. return false;
  413. }
  414. $dparts = explode('.', $domain);
  415. $base2 = implode('dc=', $dparts);
  416. if($base !== $base2 && $this->testBaseDN($base2)) {
  417. $this->applyFind('ldap_base', $base2);
  418. return $this->result;
  419. }
  420. return false;
  421. }
  422. /**
  423. * @brief sets the found value for the configuration key in the WizardResult
  424. * as well as in the Configuration instance
  425. * @param $key the configuration key
  426. * @param $value the (detected) value
  427. * @return null
  428. *
  429. */
  430. private function applyFind($key, $value) {
  431. $this->result->addChange($key, $value);
  432. $this->configuration->setConfiguration(array($key => $value));
  433. }
  434. /**
  435. * @brief Checks, whether a port was entered in the Host configuration
  436. * field. In this case the port will be stripped off, but also stored as
  437. * setting.
  438. */
  439. private function checkHost() {
  440. $host = $this->configuration->ldapHost;
  441. $hostInfo = parse_url($host);
  442. //removes Port from Host
  443. if(is_array($hostInfo) && isset($hostInfo['port'])) {
  444. $port = $hostInfo['port'];
  445. $host = str_replace(':'.$port, '', $host);
  446. $this->applyFind('ldap_host', $host);
  447. $this->applyFind('ldap_port', $port);
  448. }
  449. }
  450. /**
  451. * @brief tries to detect the group member association attribute which is
  452. * one of 'uniqueMember', 'memberUid', 'member'
  453. * @return mixed, string with the attribute name, false on error
  454. */
  455. private function detectGroupMemberAssoc() {
  456. $possibleAttrs = array('uniqueMember', 'memberUid', 'member', 'unfugasdfasdfdfa');
  457. $filter = $this->configuration->ldapGroupFilter;
  458. if(empty($filter)) {
  459. return false;
  460. }
  461. $cr = $this->getConnection();
  462. if(!$cr) {
  463. throw new \Exception('Could not connect to LDAP');
  464. }
  465. $base = $this->configuration->ldapBase[0];
  466. $rr = $this->ldap->search($cr, $base, $filter, $possibleAttrs);
  467. if(!$this->ldap->isResource($rr)) {
  468. return false;
  469. }
  470. $er = $this->ldap->firstEntry($cr, $rr);
  471. while(is_resource($er)) {
  472. $dn = $this->ldap->getDN($cr, $er);
  473. $attrs = $this->ldap->getAttributes($cr, $er);
  474. $result = array();
  475. for($i = 0; $i < count($possibleAttrs); $i++) {
  476. if(isset($attrs[$possibleAttrs[$i]])) {
  477. $result[$possibleAttrs[$i]] = $attrs[$possibleAttrs[$i]]['count'];
  478. }
  479. }
  480. if(!empty($result)) {
  481. natsort($result);
  482. return key($result);
  483. }
  484. $er = $this->ldap->nextEntry($cr, $er);
  485. }
  486. return false;
  487. }
  488. /**
  489. * @brief Checks whether for a given BaseDN results will be returned
  490. * @param $base the BaseDN to test
  491. * @return bool true on success, false otherwise
  492. */
  493. private function testBaseDN($base) {
  494. $cr = $this->getConnection();
  495. if(!$cr) {
  496. throw new \Exception('Could not connect to LDAP');
  497. }
  498. //base is there, let's validate it. If we search for anything, we should
  499. //get a result set > 0 on a proper base
  500. $rr = $this->ldap->search($cr, $base, 'objectClass=*', array('dn'), 0, 1);
  501. if(!$this->ldap->isResource($rr)) {
  502. return false;
  503. }
  504. $entries = $this->ldap->countEntries($cr, $rr);
  505. return ($entries !== false) && ($entries > 0);
  506. }
  507. /**
  508. * @brief Checks whether the server supports memberOf in LDAP Filter.
  509. * Requires that groups are determined, thus internally called from within
  510. * determineGroups()
  511. * @return bool, true if it does, false otherwise
  512. */
  513. private function testMemberOf() {
  514. $cr = $this->getConnection();
  515. if(!$cr) {
  516. throw new \Exception('Could not connect to LDAP');
  517. }
  518. if(!is_array($this->configuration->ldapBase)
  519. || !isset($this->configuration->ldapBase[0])) {
  520. return false;
  521. }
  522. $base = $this->configuration->ldapBase[0];
  523. $filterPrefix = '(&(objectclass=*)(memberOf=';
  524. $filterSuffix = '))';
  525. foreach($this->resultCache as $dn => $properties) {
  526. if(!isset($properties['cn'])) {
  527. //assuming only groups have their cn cached :)
  528. continue;
  529. }
  530. $filter = strtolower($filterPrefix . $dn . $filterSuffix);
  531. $rr = $this->ldap->search($cr, $base, $filter, array('dn'));
  532. if(!$this->ldap->isResource($rr)) {
  533. continue;
  534. }
  535. $entries = $this->ldap->countEntries($cr, $rr);
  536. //we do not know which groups are empty, so test any and return
  537. //success on the first match that returns at least one user
  538. if(($entries !== false) && ($entries > 0)) {
  539. return true;
  540. }
  541. }
  542. return false;
  543. }
  544. /**
  545. * @brief creates an LDAP Filter from given configuration
  546. * @param $filterType int, for which use case the filter shall be created
  547. * can be any of self::LFILTER_USER_LIST, self::LFILTER_LOGIN or
  548. * self::LFILTER_GROUP_LIST
  549. * @return mixed, string with the filter on success, false otherwise
  550. */
  551. private function composeLdapFilter($filterType) {
  552. $filter = '';
  553. $parts = 0;
  554. switch ($filterType) {
  555. case self::LFILTER_USER_LIST:
  556. $objcs = $this->configuration->ldapUserFilterObjectclass;
  557. //glue objectclasses
  558. if(is_array($objcs) && count($objcs) > 0) {
  559. $filter .= '(|';
  560. foreach($objcs as $objc) {
  561. $filter .= '(objectclass=' . $objc . ')';
  562. }
  563. $filter .= ')';
  564. $parts++;
  565. }
  566. //glue group memberships
  567. if($this->configuration->hasMemberOfFilterSupport) {
  568. $cns = $this->configuration->ldapUserFilterGroups;
  569. if(is_array($cns) && count($cns) > 0) {
  570. $filter .= '(|';
  571. $cr = $this->getConnection();
  572. if(!$cr) {
  573. throw new \Exception('Could not connect to LDAP');
  574. }
  575. $base = $this->configuration->ldapBase[0];
  576. foreach($cns as $cn) {
  577. $rr = $this->ldap->search($cr, $base, 'cn=' . $cn, array('dn'));
  578. if(!$this->ldap->isResource($rr)) {
  579. continue;
  580. }
  581. $er = $this->ldap->firstEntry($cr, $rr);
  582. $dn = $this->ldap->getDN($cr, $er);
  583. $filter .= '(memberof=' . $dn . ')';
  584. }
  585. $filter .= ')';
  586. }
  587. $parts++;
  588. }
  589. //wrap parts in AND condition
  590. if($parts > 1) {
  591. $filter = '(&' . $filter . ')';
  592. }
  593. if(empty($filter)) {
  594. $filter = '(objectclass=*)';
  595. }
  596. break;
  597. case self::LFILTER_GROUP_LIST:
  598. $objcs = $this->configuration->ldapGroupFilterObjectclass;
  599. //glue objectclasses
  600. if(is_array($objcs) && count($objcs) > 0) {
  601. $filter .= '(|';
  602. foreach($objcs as $objc) {
  603. $filter .= '(objectclass=' . $objc . ')';
  604. }
  605. $filter .= ')';
  606. $parts++;
  607. }
  608. //glue group memberships
  609. $cns = $this->configuration->ldapGroupFilterGroups;
  610. if(is_array($cns) && count($cns) > 0) {
  611. $filter .= '(|';
  612. $base = $this->configuration->ldapBase[0];
  613. foreach($cns as $cn) {
  614. $filter .= '(cn=' . $cn . ')';
  615. }
  616. $filter .= ')';
  617. }
  618. $parts++;
  619. //wrap parts in AND condition
  620. if($parts > 1) {
  621. $filter = '(&' . $filter . ')';
  622. }
  623. break;
  624. case self::LFILTER_LOGIN:
  625. $ulf = $this->configuration->ldapUserFilter;
  626. $loginpart = '=%uid';
  627. $filterUsername = '';
  628. $userAttributes = $this->getUserAttributes();
  629. $userAttributes = array_change_key_case(array_flip($userAttributes));
  630. $parts = 0;
  631. $x = $this->configuration->ldapLoginFilterUsername;
  632. if($this->configuration->ldapLoginFilterUsername === '1') {
  633. $attr = '';
  634. if(isset($userAttributes['uid'])) {
  635. $attr = 'uid';
  636. } else if(isset($userAttributes['samaccountname'])) {
  637. $attr = 'samaccountname';
  638. } else if(isset($userAttributes['cn'])) {
  639. //fallback
  640. $attr = 'cn';
  641. }
  642. if(!empty($attr)) {
  643. $filterUsername = '(' . $attr . $loginpart . ')';
  644. $parts++;
  645. }
  646. }
  647. $filterEmail = '';
  648. if($this->configuration->ldapLoginFilterEmail === '1') {
  649. $filterEmail = '(|(mailPrimaryAddress=%uid)(mail=%uid))';
  650. $parts++;
  651. }
  652. $filterAttributes = '';
  653. $attrsToFilter = $this->configuration->ldapLoginFilterAttributes;
  654. if(is_array($attrsToFilter) && count($attrsToFilter) > 0) {
  655. $filterAttributes = '(|';
  656. foreach($attrsToFilter as $attribute) {
  657. $filterAttributes .= '(' . $attribute . $loginpart . ')';
  658. }
  659. $filterAttributes .= ')';
  660. $parts++;
  661. }
  662. $filterLogin = '';
  663. if($parts > 1) {
  664. $filterLogin = '(|';
  665. }
  666. $filterLogin .= $filterUsername;
  667. $filterLogin .= $filterEmail;
  668. $filterLogin .= $filterAttributes;
  669. if($parts > 1) {
  670. $filterLogin .= ')';
  671. }
  672. $filter = '(&'.$ulf.$filterLogin.')';
  673. break;
  674. }
  675. \OCP\Util::writeLog('user_ldap', 'Wiz: Final filter '.$filter, \OCP\Util::DEBUG);
  676. return $filter;
  677. }
  678. /**
  679. * Connects and Binds to an LDAP Server
  680. * @param $port the port to connect with
  681. * @param $tls whether startTLS is to be used
  682. * @return
  683. */
  684. private function connectAndBind($port = 389, $tls = false, $ncc = false) {
  685. if($ncc) {
  686. //No certificate check
  687. //FIXME: undo afterwards
  688. putenv('LDAPTLS_REQCERT=never');
  689. }
  690. //connect, does not really trigger any server communication
  691. \OCP\Util::writeLog('user_ldap', 'Wiz: Checking Host Info ', \OCP\Util::DEBUG);
  692. $host = $this->configuration->ldapHost;
  693. $hostInfo = parse_url($host);
  694. if(!$hostInfo) {
  695. throw new \Exception($this->l->t('Invalid Host'));
  696. }
  697. if(isset($hostInfo['scheme'])) {
  698. if(isset($hostInfo['port'])) {
  699. //problem
  700. } else {
  701. $host .= ':' . $port;
  702. }
  703. }
  704. \OCP\Util::writeLog('user_ldap', 'Wiz: Attempting to connect ', \OCP\Util::DEBUG);
  705. $cr = $this->ldap->connect($host, $port);
  706. if(!is_resource($cr)) {
  707. throw new \Exception($this->l->t('Invalid Host'));
  708. }
  709. \OCP\Util::writeLog('user_ldap', 'Wiz: Setting LDAP Options ', \OCP\Util::DEBUG);
  710. //set LDAP options
  711. $a = $this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
  712. $c = $this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
  713. if($tls) {
  714. $this->ldap->startTls($cr);
  715. }
  716. \OCP\Util::writeLog('user_ldap', 'Wiz: Attemping to Bind ', \OCP\Util::DEBUG);
  717. //interesting part: do the bind!
  718. $login = $this->ldap->bind($cr,
  719. $this->configuration->ldapAgentName,
  720. $this->configuration->ldapAgentPassword);
  721. if($login === true) {
  722. $this->ldap->unbind($cr);
  723. if($ncc) {
  724. throw new \Exception('Certificate cannot be validated.');
  725. }
  726. \OCP\Util::writeLog('user_ldap', 'Wiz: Bind succesfull with Port '. $port, \OCP\Util::DEBUG);
  727. return true;
  728. }
  729. $errno = $this->ldap->errno($cr);
  730. $error = ldap_error($cr);
  731. $this->ldap->unbind($cr);
  732. if($errno === -1 || ($errno === 2 && $ncc)) {
  733. //host, port or TLS wrong
  734. return false;
  735. } else if ($errno === 2) {
  736. return $this->connectAndBind($port, $tls, true);
  737. }
  738. throw new \Exception($error);
  739. }
  740. /**
  741. * @brief checks whether a valid combination of agent and password has been
  742. * provided (either two values or nothing for anonymous connect)
  743. * @return boolean, true if everything is fine, false otherwise
  744. *
  745. */
  746. private function checkAgentRequirements() {
  747. $agent = $this->configuration->ldapAgentName;
  748. $pwd = $this->configuration->ldapAgentPassword;
  749. return ( (!empty($agent) && !empty($pwd))
  750. || (empty($agent) && empty($pwd)));
  751. }
  752. private function checkRequirements($reqs) {
  753. $this->checkAgentRequirements();
  754. foreach($reqs as $option) {
  755. $value = $this->configuration->$option;
  756. if(empty($value)) {
  757. return false;
  758. }
  759. }
  760. return true;
  761. }
  762. /**
  763. * @brief does a cumulativeSearch on LDAP to get different values of a
  764. * specified attribute
  765. * @param $filters array, the filters that shall be used in the search
  766. * @param $attr the attribute of which a list of values shall be returned
  767. * @param $lfw bool, whether the last filter is a wildcard which shall not
  768. * be processed if there were already findings, defaults to true
  769. * @param $maxF string. if not null, this variable will have the filter that
  770. * yields most result entries
  771. * @return mixed, an array with the values on success, false otherwise
  772. *
  773. */
  774. private function cumulativeSearchOnAttribute($filters, $attr, $lfw = true, &$maxF = null) {
  775. $dnRead = array();
  776. $foundItems = array();
  777. $maxEntries = 0;
  778. if(!is_array($this->configuration->ldapBase)
  779. || !isset($this->configuration->ldapBase[0])) {
  780. return false;
  781. }
  782. $base = $this->configuration->ldapBase[0];
  783. $cr = $this->getConnection();
  784. if(!is_resource($cr)) {
  785. return false;
  786. }
  787. foreach($filters as $filter) {
  788. if($lfw && count($foundItems) > 0) {
  789. continue;
  790. }
  791. $rr = $this->ldap->search($cr, $base, $filter, array($attr));
  792. if(!$this->ldap->isResource($rr)) {
  793. continue;
  794. }
  795. $entries = $this->ldap->countEntries($cr, $rr);
  796. $getEntryFunc = 'firstEntry';
  797. if(($entries !== false) && ($entries > 0)) {
  798. if(!is_null($maxF) && $entries > $maxEntries) {
  799. $maxEntries = $entries;
  800. $maxF = $filter;
  801. }
  802. do {
  803. $entry = $this->ldap->$getEntryFunc($cr, $rr);
  804. if(!$this->ldap->isResource($entry)) {
  805. continue 2;
  806. }
  807. $attributes = $this->ldap->getAttributes($cr, $entry);
  808. $dn = $this->ldap->getDN($cr, $entry);
  809. if($dn === false || in_array($dn, $dnRead)) {
  810. continue;
  811. }
  812. $newItems = array();
  813. $state = $this->getAttributeValuesFromEntry($attributes,
  814. $attr,
  815. $newItems);
  816. $foundItems = array_merge($foundItems, $newItems);
  817. $this->resultCache[$dn][$attr] = $newItems;
  818. $dnRead[] = $dn;
  819. $getEntryFunc = 'nextEntry';
  820. $rr = $entry; //will be expected by nextEntry next round
  821. } while($state === self::LRESULT_PROCESSED_SKIP
  822. || $this->ldap->isResource($entry));
  823. }
  824. }
  825. return array_unique($foundItems);
  826. }
  827. /**
  828. * @brief determines if and which $attr are available on the LDAP server
  829. * @param $objectclasses the objectclasses to use as search filter
  830. * @param $attr the attribute to look for
  831. * @param $dbkey the dbkey of the setting the feature is connected to
  832. * @param $confkey the confkey counterpart for the $dbkey as used in the
  833. * Configuration class
  834. * @param $po boolean, whether the objectClass with most result entries
  835. * shall be pre-selected via the result
  836. * @returns array, list of found items.
  837. */
  838. private function determineFeature($objectclasses, $attr, $dbkey, $confkey, $po = false) {
  839. $cr = $this->getConnection();
  840. if(!$cr) {
  841. throw new \Exception('Could not connect to LDAP');
  842. }
  843. $p = 'objectclass=';
  844. foreach($objectclasses as $key => $value) {
  845. $objectclasses[$key] = $p.$value;
  846. }
  847. $maxEntryObjC = '';
  848. $availableFeatures =
  849. $this->cumulativeSearchOnAttribute($objectclasses, $attr,
  850. true, $maxEntryObjC);
  851. if(is_array($availableFeatures)
  852. && count($availableFeatures) > 0) {
  853. natcasesort($availableFeatures);
  854. //natcasesort keeps indices, but we must get rid of them for proper
  855. //sorting in the web UI. Therefore: array_values
  856. $this->result->addOptions($dbkey, array_values($availableFeatures));
  857. } else {
  858. throw new \Exception(self::$l->t('Could not find the desired feature'));
  859. }
  860. $setFeatures = $this->configuration->$confkey;
  861. if(is_array($setFeatures) && !empty($setFeatures)) {
  862. //something is already configured? pre-select it.
  863. $this->result->addChange($dbkey, $setFeatures);
  864. } else if($po && !empty($maxEntryObjC)) {
  865. //pre-select objectclass with most result entries
  866. $maxEntryObjC = str_replace($p, '', $maxEntryObjC);
  867. $this->applyFind($dbkey, $maxEntryObjC);
  868. $this->result->addChange($dbkey, $maxEntryObjC);
  869. }
  870. return $availableFeatures;
  871. }
  872. /**
  873. * @brief appends a list of values fr
  874. * @param $result resource, the return value from ldap_get_attributes
  875. * @param $attribute string, the attribute values to look for
  876. * @param &$known array, new values will be appended here
  877. * @return int, state on of the class constants LRESULT_PROCESSED_OK,
  878. * LRESULT_PROCESSED_INVALID or LRESULT_PROCESSED_SKIP
  879. */
  880. private function getAttributeValuesFromEntry($result, $attribute, &$known) {
  881. if(!is_array($result)
  882. || !isset($result['count'])
  883. || !$result['count'] > 0) {
  884. return self::LRESULT_PROCESSED_INVALID;
  885. }
  886. //strtolower on all keys for proper comparison
  887. $result = \OCP\Util::mb_array_change_key_case($result);
  888. $attribute = strtolower($attribute);
  889. if(isset($result[$attribute])) {
  890. foreach($result[$attribute] as $key => $val) {
  891. if($key === 'count') {
  892. continue;
  893. }
  894. if(!in_array($val, $known)) {
  895. $known[] = $val;
  896. }
  897. }
  898. return self::LRESULT_PROCESSED_OK;
  899. } else {
  900. return self::LRESULT_PROCESSED_SKIP;
  901. }
  902. }
  903. private function getConnection() {
  904. if(!is_null($this->cr)) {
  905. return $this->cr;
  906. }
  907. $cr = $this->ldap->connect(
  908. $this->configuration->ldapHost.':'.$this->configuration->ldapPort,
  909. $this->configuration->ldapPort);
  910. $this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
  911. $this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
  912. if($this->configuration->ldapTLS === 1) {
  913. $this->ldap->startTls($cr);
  914. }
  915. $lo = @$this->ldap->bind($cr,
  916. $this->configuration->ldapAgentName,
  917. $this->configuration->ldapAgentPassword);
  918. if($lo === true) {
  919. $this->$cr = $cr;
  920. return $cr;
  921. }
  922. return false;
  923. }
  924. private function getDefaultLdapPortSettings() {
  925. static $settings = array(
  926. array('port' => 7636, 'tls' => false),
  927. array('port' => 636, 'tls' => false),
  928. array('port' => 7389, 'tls' => true),
  929. array('port' => 389, 'tls' => true),
  930. array('port' => 7389, 'tls' => false),
  931. array('port' => 389, 'tls' => false),
  932. );
  933. return $settings;
  934. }
  935. private function getPortSettingsToTry() {
  936. //389 ← LDAP / Unencrypted or StartTLS
  937. //636 ← LDAPS / SSL
  938. //7xxx ← UCS. need to be checked first, because both ports may be open
  939. $host = $this->configuration->ldapHost;
  940. $port = intval($this->configuration->ldapPort);
  941. $portSettings = array();
  942. //In case the port is already provided, we will check this first
  943. if($port > 0) {
  944. $hostInfo = parse_url($host);
  945. if(!(is_array($hostInfo)
  946. && isset($hostInfo['scheme'])
  947. && stripos($hostInfo['scheme'], 'ldaps') !== false)) {
  948. $portSettings[] = array('port' => $port, 'tls' => true);
  949. }
  950. $portSettings[] =array('port' => $port, 'tls' => false);
  951. }
  952. //default ports
  953. $portSettings = array_merge($portSettings,
  954. $this->getDefaultLdapPortSettings());
  955. return $portSettings;
  956. }
  957. }