wizard.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302
  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. }