User.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\User_LDAP\User;
  8. use InvalidArgumentException;
  9. use OC\Accounts\AccountManager;
  10. use OCA\User_LDAP\Access;
  11. use OCA\User_LDAP\Connection;
  12. use OCA\User_LDAP\Exceptions\AttributeNotSet;
  13. use OCA\User_LDAP\FilesystemHelper;
  14. use OCA\User_LDAP\Service\BirthdateParserService;
  15. use OCP\Accounts\IAccountManager;
  16. use OCP\Accounts\PropertyDoesNotExistException;
  17. use OCP\IAvatarManager;
  18. use OCP\IConfig;
  19. use OCP\Image;
  20. use OCP\IUser;
  21. use OCP\IUserManager;
  22. use OCP\Notification\IManager as INotificationManager;
  23. use OCP\PreConditionNotMetException;
  24. use OCP\Server;
  25. use OCP\Util;
  26. use Psr\Log\LoggerInterface;
  27. /**
  28. * User
  29. *
  30. * represents an LDAP user, gets and holds user-specific information from LDAP
  31. */
  32. class User {
  33. /**
  34. * @var Connection
  35. */
  36. protected $connection;
  37. /**
  38. * @var LoggerInterface
  39. */
  40. protected $logger;
  41. /**
  42. * @var string
  43. */
  44. protected $dn;
  45. /**
  46. * @var string
  47. */
  48. protected $uid;
  49. /**
  50. * @var string[]
  51. */
  52. protected $refreshedFeatures = [];
  53. /**
  54. * @var string
  55. */
  56. protected $avatarImage;
  57. protected BirthdateParserService $birthdateParser;
  58. /**
  59. * DB config keys for user preferences
  60. */
  61. public const USER_PREFKEY_FIRSTLOGIN = 'firstLoginAccomplished';
  62. /**
  63. * @brief constructor, make sure the subclasses call this one!
  64. * @param string $username the internal username
  65. * @param string $dn the LDAP DN
  66. */
  67. public function __construct(
  68. $username,
  69. $dn,
  70. protected Access $access,
  71. protected IConfig $config,
  72. protected FilesystemHelper $fs,
  73. protected Image $image,
  74. LoggerInterface $logger,
  75. protected IAvatarManager $avatarManager,
  76. protected IUserManager $userManager,
  77. protected INotificationManager $notificationManager,
  78. ) {
  79. if ($username === null) {
  80. $logger->error("uid for '$dn' must not be null!", ['app' => 'user_ldap']);
  81. throw new \InvalidArgumentException('uid must not be null!');
  82. } elseif ($username === '') {
  83. $logger->error("uid for '$dn' must not be an empty string", ['app' => 'user_ldap']);
  84. throw new \InvalidArgumentException('uid must not be an empty string!');
  85. }
  86. $this->connection = $this->access->getConnection();
  87. $this->dn = $dn;
  88. $this->uid = $username;
  89. $this->logger = $logger;
  90. $this->birthdateParser = new BirthdateParserService();
  91. Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry');
  92. }
  93. /**
  94. * marks a user as deleted
  95. *
  96. * @throws PreConditionNotMetException
  97. */
  98. public function markUser() {
  99. $curValue = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '0');
  100. if ($curValue === '1') {
  101. // the user is already marked, do not write to DB again
  102. return;
  103. }
  104. $this->config->setUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '1');
  105. $this->config->setUserValue($this->getUsername(), 'user_ldap', 'foundDeleted', (string)time());
  106. }
  107. /**
  108. * processes results from LDAP for attributes as returned by getAttributesToRead()
  109. * @param array $ldapEntry the user entry as retrieved from LDAP
  110. */
  111. public function processAttributes($ldapEntry) {
  112. //Quota
  113. $attr = strtolower($this->connection->ldapQuotaAttribute);
  114. if (isset($ldapEntry[$attr])) {
  115. $this->updateQuota($ldapEntry[$attr][0]);
  116. } else {
  117. if ($this->connection->ldapQuotaDefault !== '') {
  118. $this->updateQuota();
  119. }
  120. }
  121. unset($attr);
  122. //displayName
  123. $displayName = $displayName2 = '';
  124. $attr = strtolower($this->connection->ldapUserDisplayName);
  125. if (isset($ldapEntry[$attr])) {
  126. $displayName = (string)$ldapEntry[$attr][0];
  127. }
  128. $attr = strtolower($this->connection->ldapUserDisplayName2);
  129. if (isset($ldapEntry[$attr])) {
  130. $displayName2 = (string)$ldapEntry[$attr][0];
  131. }
  132. if ($displayName !== '') {
  133. $this->composeAndStoreDisplayName($displayName, $displayName2);
  134. $this->access->cacheUserDisplayName(
  135. $this->getUsername(),
  136. $displayName,
  137. $displayName2
  138. );
  139. }
  140. unset($attr);
  141. //Email
  142. //email must be stored after displayname, because it would cause a user
  143. //change event that will trigger fetching the display name again
  144. $attr = strtolower($this->connection->ldapEmailAttribute);
  145. if (isset($ldapEntry[$attr])) {
  146. $this->updateEmail($ldapEntry[$attr][0]);
  147. }
  148. unset($attr);
  149. // LDAP Username, needed for s2s sharing
  150. if (isset($ldapEntry['uid'])) {
  151. $this->storeLDAPUserName($ldapEntry['uid'][0]);
  152. } elseif (isset($ldapEntry['samaccountname'])) {
  153. $this->storeLDAPUserName($ldapEntry['samaccountname'][0]);
  154. }
  155. //homePath
  156. if (str_starts_with($this->connection->homeFolderNamingRule, 'attr:')) {
  157. $attr = strtolower(substr($this->connection->homeFolderNamingRule, strlen('attr:')));
  158. if (isset($ldapEntry[$attr])) {
  159. $this->access->cacheUserHome(
  160. $this->getUsername(), $this->getHomePath($ldapEntry[$attr][0]));
  161. }
  162. }
  163. //memberOf groups
  164. $cacheKey = 'getMemberOf' . $this->getUsername();
  165. $groups = false;
  166. if (isset($ldapEntry['memberof'])) {
  167. $groups = $ldapEntry['memberof'];
  168. }
  169. $this->connection->writeToCache($cacheKey, $groups);
  170. //external storage var
  171. $attr = strtolower($this->connection->ldapExtStorageHomeAttribute);
  172. if (isset($ldapEntry[$attr])) {
  173. $this->updateExtStorageHome($ldapEntry[$attr][0]);
  174. }
  175. unset($attr);
  176. // check for cached profile data
  177. $username = $this->getUsername(); // buffer variable, to save resource
  178. $cacheKey = 'getUserProfile-' . $username;
  179. $profileCached = $this->connection->getFromCache($cacheKey);
  180. // honoring profile disabled in config.php and check if user profile was refreshed
  181. if ($this->config->getSystemValueBool('profile.enabled', true) &&
  182. ($profileCached === null) && // no cache or TTL not expired
  183. !$this->wasRefreshed('profile')) {
  184. // check current data
  185. $profileValues = [];
  186. //User Profile Field - Phone number
  187. $attr = strtolower($this->connection->ldapAttributePhone);
  188. if (!empty($attr)) { // attribute configured
  189. $profileValues[IAccountManager::PROPERTY_PHONE]
  190. = $ldapEntry[$attr][0] ?? '';
  191. }
  192. //User Profile Field - website
  193. $attr = strtolower($this->connection->ldapAttributeWebsite);
  194. if (isset($ldapEntry[$attr])) {
  195. $cutPosition = strpos($ldapEntry[$attr][0], ' ');
  196. if ($cutPosition) {
  197. // drop appended label
  198. $profileValues[IAccountManager::PROPERTY_WEBSITE]
  199. = substr($ldapEntry[$attr][0], 0, $cutPosition);
  200. } else {
  201. $profileValues[IAccountManager::PROPERTY_WEBSITE]
  202. = $ldapEntry[$attr][0];
  203. }
  204. } elseif (!empty($attr)) { // configured, but not defined
  205. $profileValues[IAccountManager::PROPERTY_WEBSITE] = '';
  206. }
  207. //User Profile Field - Address
  208. $attr = strtolower($this->connection->ldapAttributeAddress);
  209. if (isset($ldapEntry[$attr])) {
  210. if (str_contains($ldapEntry[$attr][0], '$')) {
  211. // basic format conversion from postalAddress syntax to commata delimited
  212. $profileValues[IAccountManager::PROPERTY_ADDRESS]
  213. = str_replace('$', ', ', $ldapEntry[$attr][0]);
  214. } else {
  215. $profileValues[IAccountManager::PROPERTY_ADDRESS]
  216. = $ldapEntry[$attr][0];
  217. }
  218. } elseif (!empty($attr)) { // configured, but not defined
  219. $profileValues[IAccountManager::PROPERTY_ADDRESS] = '';
  220. }
  221. //User Profile Field - Twitter
  222. $attr = strtolower($this->connection->ldapAttributeTwitter);
  223. if (!empty($attr)) {
  224. $profileValues[IAccountManager::PROPERTY_TWITTER]
  225. = $ldapEntry[$attr][0] ?? '';
  226. }
  227. //User Profile Field - fediverse
  228. $attr = strtolower($this->connection->ldapAttributeFediverse);
  229. if (!empty($attr)) {
  230. $profileValues[IAccountManager::PROPERTY_FEDIVERSE]
  231. = $ldapEntry[$attr][0] ?? '';
  232. }
  233. //User Profile Field - organisation
  234. $attr = strtolower($this->connection->ldapAttributeOrganisation);
  235. if (!empty($attr)) {
  236. $profileValues[IAccountManager::PROPERTY_ORGANISATION]
  237. = $ldapEntry[$attr][0] ?? '';
  238. }
  239. //User Profile Field - role
  240. $attr = strtolower($this->connection->ldapAttributeRole);
  241. if (!empty($attr)) {
  242. $profileValues[IAccountManager::PROPERTY_ROLE]
  243. = $ldapEntry[$attr][0] ?? '';
  244. }
  245. //User Profile Field - headline
  246. $attr = strtolower($this->connection->ldapAttributeHeadline);
  247. if (!empty($attr)) {
  248. $profileValues[IAccountManager::PROPERTY_HEADLINE]
  249. = $ldapEntry[$attr][0] ?? '';
  250. }
  251. //User Profile Field - biography
  252. $attr = strtolower($this->connection->ldapAttributeBiography);
  253. if (isset($ldapEntry[$attr])) {
  254. if (str_contains($ldapEntry[$attr][0], '\r')) {
  255. // convert line endings
  256. $profileValues[IAccountManager::PROPERTY_BIOGRAPHY]
  257. = str_replace(["\r\n","\r"], "\n", $ldapEntry[$attr][0]);
  258. } else {
  259. $profileValues[IAccountManager::PROPERTY_BIOGRAPHY]
  260. = $ldapEntry[$attr][0];
  261. }
  262. } elseif (!empty($attr)) { // configured, but not defined
  263. $profileValues[IAccountManager::PROPERTY_BIOGRAPHY] = '';
  264. }
  265. //User Profile Field - birthday
  266. $attr = strtolower($this->connection->ldapAttributeBirthDate);
  267. if (!empty($attr) && !empty($ldapEntry[$attr][0])) {
  268. $value = $ldapEntry[$attr][0];
  269. try {
  270. $birthdate = $this->birthdateParser->parseBirthdate($value);
  271. $profileValues[IAccountManager::PROPERTY_BIRTHDATE]
  272. = $birthdate->format('Y-m-d');
  273. } catch (InvalidArgumentException $e) {
  274. // Invalid date -> just skip the property
  275. $this->logger->info("Failed to parse user's birthdate from LDAP: $value", [
  276. 'exception' => $e,
  277. 'userId' => $username,
  278. ]);
  279. }
  280. }
  281. //User Profile Field - pronouns
  282. $attr = strtolower($this->connection->ldapAttributePronouns);
  283. if (!empty($attr)) {
  284. $profileValues[IAccountManager::PROPERTY_PRONOUNS]
  285. = $ldapEntry[$attr][0] ?? '';
  286. }
  287. // check for changed data and cache just for TTL checking
  288. $checksum = hash('sha256', json_encode($profileValues));
  289. $this->connection->writeToCache($cacheKey, $checksum // write array to cache. is waste of cache space
  290. , null); // use ldapCacheTTL from configuration
  291. // Update user profile
  292. if ($this->config->getUserValue($username, 'user_ldap', 'lastProfileChecksum', null) !== $checksum) {
  293. $this->config->setUserValue($username, 'user_ldap', 'lastProfileChecksum', $checksum);
  294. $this->updateProfile($profileValues);
  295. $this->logger->info("updated profile uid=$username", ['app' => 'user_ldap']);
  296. } else {
  297. $this->logger->debug('profile data from LDAP unchanged', ['app' => 'user_ldap', 'uid' => $username]);
  298. }
  299. unset($attr);
  300. } elseif ($profileCached !== null) { // message delayed, to declutter log
  301. $this->logger->debug('skipping profile check, while cached data exist', ['app' => 'user_ldap', 'uid' => $username]);
  302. }
  303. //Avatar
  304. /** @var Connection $connection */
  305. $connection = $this->access->getConnection();
  306. $attributes = $connection->resolveRule('avatar');
  307. foreach ($attributes as $attribute) {
  308. if (isset($ldapEntry[$attribute])) {
  309. $this->avatarImage = $ldapEntry[$attribute][0];
  310. // the call to the method that saves the avatar in the file
  311. // system must be postponed after the login. It is to ensure
  312. // external mounts are mounted properly (e.g. with login
  313. // credentials from the session).
  314. Util::connectHook('OC_User', 'post_login', $this, 'updateAvatarPostLogin');
  315. break;
  316. }
  317. }
  318. }
  319. /**
  320. * @brief returns the LDAP DN of the user
  321. * @return string
  322. */
  323. public function getDN() {
  324. return $this->dn;
  325. }
  326. /**
  327. * @brief returns the Nextcloud internal username of the user
  328. * @return string
  329. */
  330. public function getUsername() {
  331. return $this->uid;
  332. }
  333. /**
  334. * returns the home directory of the user if specified by LDAP settings
  335. * @param ?string $valueFromLDAP
  336. * @return false|string
  337. * @throws \Exception
  338. */
  339. public function getHomePath($valueFromLDAP = null) {
  340. $path = (string)$valueFromLDAP;
  341. $attr = null;
  342. if (is_null($valueFromLDAP)
  343. && str_starts_with($this->access->connection->homeFolderNamingRule, 'attr:')
  344. && $this->access->connection->homeFolderNamingRule !== 'attr:') {
  345. $attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:'));
  346. $homedir = $this->access->readAttribute($this->access->username2dn($this->getUsername()), $attr);
  347. if ($homedir && isset($homedir[0])) {
  348. $path = $homedir[0];
  349. }
  350. }
  351. if ($path !== '') {
  352. //if attribute's value is an absolute path take this, otherwise append it to data dir
  353. //check for / at the beginning or pattern c:\ resp. c:/
  354. if ($path[0] !== '/'
  355. && !(strlen($path) > 3 && ctype_alpha($path[0])
  356. && $path[1] === ':' && ($path[2] === '\\' || $path[2] === '/'))
  357. ) {
  358. $path = $this->config->getSystemValue('datadirectory',
  359. \OC::$SERVERROOT . '/data') . '/' . $path;
  360. }
  361. //we need it to store it in the DB as well in case a user gets
  362. //deleted so we can clean up afterwards
  363. $this->config->setUserValue(
  364. $this->getUsername(), 'user_ldap', 'homePath', $path
  365. );
  366. return $path;
  367. }
  368. if (!is_null($attr)
  369. && $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', 'true')
  370. ) {
  371. // a naming rule attribute is defined, but it doesn't exist for that LDAP user
  372. throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $this->getUsername());
  373. }
  374. //false will apply default behaviour as defined and done by OC_User
  375. $this->config->setUserValue($this->getUsername(), 'user_ldap', 'homePath', '');
  376. return false;
  377. }
  378. public function getMemberOfGroups() {
  379. $cacheKey = 'getMemberOf' . $this->getUsername();
  380. $memberOfGroups = $this->connection->getFromCache($cacheKey);
  381. if (!is_null($memberOfGroups)) {
  382. return $memberOfGroups;
  383. }
  384. $groupDNs = $this->access->readAttribute($this->getDN(), 'memberOf');
  385. $this->connection->writeToCache($cacheKey, $groupDNs);
  386. return $groupDNs;
  387. }
  388. /**
  389. * @brief reads the image from LDAP that shall be used as Avatar
  390. * @return string data (provided by LDAP) | false
  391. */
  392. public function getAvatarImage() {
  393. if (!is_null($this->avatarImage)) {
  394. return $this->avatarImage;
  395. }
  396. $this->avatarImage = false;
  397. /** @var Connection $connection */
  398. $connection = $this->access->getConnection();
  399. $attributes = $connection->resolveRule('avatar');
  400. foreach ($attributes as $attribute) {
  401. $result = $this->access->readAttribute($this->dn, $attribute);
  402. if ($result !== false && is_array($result) && isset($result[0])) {
  403. $this->avatarImage = $result[0];
  404. break;
  405. }
  406. }
  407. return $this->avatarImage;
  408. }
  409. /**
  410. * @brief marks the user as having logged in at least once
  411. * @return null
  412. */
  413. public function markLogin() {
  414. $this->config->setUserValue(
  415. $this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, '1');
  416. }
  417. /**
  418. * Stores a key-value pair in relation to this user
  419. *
  420. * @param string $key
  421. * @param string $value
  422. */
  423. private function store($key, $value) {
  424. $this->config->setUserValue($this->uid, 'user_ldap', $key, $value);
  425. }
  426. /**
  427. * Composes the display name and stores it in the database. The final
  428. * display name is returned.
  429. *
  430. * @param string $displayName
  431. * @param string $displayName2
  432. * @return string the effective display name
  433. */
  434. public function composeAndStoreDisplayName($displayName, $displayName2 = '') {
  435. $displayName2 = (string)$displayName2;
  436. if ($displayName2 !== '') {
  437. $displayName .= ' (' . $displayName2 . ')';
  438. }
  439. $oldName = $this->config->getUserValue($this->uid, 'user_ldap', 'displayName', null);
  440. if ($oldName !== $displayName) {
  441. $this->store('displayName', $displayName);
  442. $user = $this->userManager->get($this->getUsername());
  443. if (!empty($oldName) && $user instanceof \OC\User\User) {
  444. // if it was empty, it would be a new record, not a change emitting the trigger could
  445. // potentially cause a UniqueConstraintViolationException, depending on some factors.
  446. $user->triggerChange('displayName', $displayName, $oldName);
  447. }
  448. }
  449. return $displayName;
  450. }
  451. /**
  452. * Stores the LDAP Username in the Database
  453. * @param string $userName
  454. */
  455. public function storeLDAPUserName($userName) {
  456. $this->store('uid', $userName);
  457. }
  458. /**
  459. * @brief checks whether an update method specified by feature was run
  460. * already. If not, it will marked like this, because it is expected that
  461. * the method will be run, when false is returned.
  462. * @param string $feature email | quota | avatar | profile (can be extended)
  463. * @return bool
  464. */
  465. private function wasRefreshed($feature) {
  466. if (isset($this->refreshedFeatures[$feature])) {
  467. return true;
  468. }
  469. $this->refreshedFeatures[$feature] = 1;
  470. return false;
  471. }
  472. /**
  473. * fetches the email from LDAP and stores it as Nextcloud user value
  474. * @param string $valueFromLDAP if known, to save an LDAP read request
  475. * @return null
  476. */
  477. public function updateEmail($valueFromLDAP = null) {
  478. if ($this->wasRefreshed('email')) {
  479. return;
  480. }
  481. $email = (string)$valueFromLDAP;
  482. if (is_null($valueFromLDAP)) {
  483. $emailAttribute = $this->connection->ldapEmailAttribute;
  484. if ($emailAttribute !== '') {
  485. $aEmail = $this->access->readAttribute($this->dn, $emailAttribute);
  486. if (is_array($aEmail) && (count($aEmail) > 0)) {
  487. $email = (string)$aEmail[0];
  488. }
  489. }
  490. }
  491. if ($email !== '') {
  492. $user = $this->userManager->get($this->uid);
  493. if (!is_null($user)) {
  494. $currentEmail = (string)$user->getSystemEMailAddress();
  495. if ($currentEmail !== $email) {
  496. $user->setEMailAddress($email);
  497. }
  498. }
  499. }
  500. }
  501. /**
  502. * Overall process goes as follow:
  503. * 1. fetch the quota from LDAP and check if it's parseable with the "verifyQuotaValue" function
  504. * 2. if the value can't be fetched, is empty or not parseable, use the default LDAP quota
  505. * 3. if the default LDAP quota can't be parsed, use the Nextcloud's default quota (use 'default')
  506. * 4. check if the target user exists and set the quota for the user.
  507. *
  508. * In order to improve performance and prevent an unwanted extra LDAP call, the $valueFromLDAP
  509. * parameter can be passed with the value of the attribute. This value will be considered as the
  510. * quota for the user coming from the LDAP server (step 1 of the process) It can be useful to
  511. * fetch all the user's attributes in one call and use the fetched values in this function.
  512. * The expected value for that parameter is a string describing the quota for the user. Valid
  513. * values are 'none' (unlimited), 'default' (the Nextcloud's default quota), '1234' (quota in
  514. * bytes), '1234 MB' (quota in MB - check the \OC_Helper::computerFileSize method for more info)
  515. *
  516. * fetches the quota from LDAP and stores it as Nextcloud user value
  517. * @param ?string $valueFromLDAP the quota attribute's value can be passed,
  518. * to save the readAttribute request
  519. * @return void
  520. */
  521. public function updateQuota($valueFromLDAP = null) {
  522. if ($this->wasRefreshed('quota')) {
  523. return;
  524. }
  525. $quotaAttribute = $this->connection->ldapQuotaAttribute;
  526. $defaultQuota = $this->connection->ldapQuotaDefault;
  527. if ($quotaAttribute === '' && $defaultQuota === '') {
  528. return;
  529. }
  530. $quota = false;
  531. if (is_null($valueFromLDAP) && $quotaAttribute !== '') {
  532. $aQuota = $this->access->readAttribute($this->dn, $quotaAttribute);
  533. if ($aQuota && (count($aQuota) > 0) && $this->verifyQuotaValue($aQuota[0])) {
  534. $quota = $aQuota[0];
  535. } elseif (is_array($aQuota) && isset($aQuota[0])) {
  536. $this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . $aQuota[0] . ']', ['app' => 'user_ldap']);
  537. }
  538. } elseif (!is_null($valueFromLDAP) && $this->verifyQuotaValue($valueFromLDAP)) {
  539. $quota = $valueFromLDAP;
  540. } else {
  541. $this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . $valueFromLDAP . ']', ['app' => 'user_ldap']);
  542. }
  543. if ($quota === false && $this->verifyQuotaValue($defaultQuota)) {
  544. // quota not found using the LDAP attribute (or not parseable). Try the default quota
  545. $quota = $defaultQuota;
  546. } elseif ($quota === false) {
  547. $this->logger->debug('no suitable default quota found for user ' . $this->uid . ': [' . $defaultQuota . ']', ['app' => 'user_ldap']);
  548. return;
  549. }
  550. $targetUser = $this->userManager->get($this->uid);
  551. if ($targetUser instanceof IUser) {
  552. $targetUser->setQuota($quota);
  553. } else {
  554. $this->logger->info('trying to set a quota for user ' . $this->uid . ' but the user is missing', ['app' => 'user_ldap']);
  555. }
  556. }
  557. private function verifyQuotaValue(string $quotaValue) {
  558. return $quotaValue === 'none' || $quotaValue === 'default' || \OC_Helper::computerFileSize($quotaValue) !== false;
  559. }
  560. /**
  561. * takes values from LDAP and stores it as Nextcloud user profile value
  562. *
  563. * @param array $profileValues associative array of property keys and values from LDAP
  564. */
  565. private function updateProfile(array $profileValues): void {
  566. // check if given array is empty
  567. if (empty($profileValues)) {
  568. return; // okay, nothing to do
  569. }
  570. // fetch/prepare user
  571. $user = $this->userManager->get($this->uid);
  572. if (is_null($user)) {
  573. $this->logger->error('could not get user for uid=' . $this->uid . '', ['app' => 'user_ldap']);
  574. return;
  575. }
  576. // prepare AccountManager and Account
  577. $accountManager = Server::get(IAccountManager::class);
  578. $account = $accountManager->getAccount($user); // get Account
  579. $defaultScopes = array_merge(AccountManager::DEFAULT_SCOPES,
  580. $this->config->getSystemValue('account_manager.default_property_scope', []));
  581. // loop through the properties and handle them
  582. foreach ($profileValues as $property => $valueFromLDAP) {
  583. // check and update profile properties
  584. $value = (is_array($valueFromLDAP) ? $valueFromLDAP[0] : $valueFromLDAP); // take ONLY the first value, if multiple values specified
  585. try {
  586. $accountProperty = $account->getProperty($property);
  587. $currentValue = $accountProperty->getValue();
  588. $scope = ($accountProperty->getScope() ?: $defaultScopes[$property]);
  589. } catch (PropertyDoesNotExistException $e) { // thrown at getProperty
  590. $this->logger->error('property does not exist: ' . $property
  591. . ' for uid=' . $this->uid . '', ['app' => 'user_ldap', 'exception' => $e]);
  592. $currentValue = '';
  593. $scope = $defaultScopes[$property];
  594. }
  595. $verified = IAccountManager::VERIFIED; // trust the LDAP admin knew what they put there
  596. if ($currentValue !== $value) {
  597. $account->setProperty($property, $value, $scope, $verified);
  598. $this->logger->debug('update user profile: ' . $property . '=' . $value
  599. . ' for uid=' . $this->uid . '', ['app' => 'user_ldap']);
  600. }
  601. }
  602. try {
  603. $accountManager->updateAccount($account); // may throw InvalidArgumentException
  604. } catch (\InvalidArgumentException $e) {
  605. $this->logger->error('invalid data from LDAP: for uid=' . $this->uid . '', ['app' => 'user_ldap', 'func' => 'updateProfile'
  606. , 'exception' => $e]);
  607. }
  608. }
  609. /**
  610. * called by a post_login hook to save the avatar picture
  611. *
  612. * @param array $params
  613. */
  614. public function updateAvatarPostLogin($params) {
  615. if (isset($params['uid']) && $params['uid'] === $this->getUsername()) {
  616. $this->updateAvatar();
  617. }
  618. }
  619. /**
  620. * @brief attempts to get an image from LDAP and sets it as Nextcloud avatar
  621. * @return bool true when the avatar was set successfully or is up to date
  622. */
  623. public function updateAvatar(bool $force = false): bool {
  624. if (!$force && $this->wasRefreshed('avatar')) {
  625. return false;
  626. }
  627. $avatarImage = $this->getAvatarImage();
  628. if ($avatarImage === false) {
  629. //not set, nothing left to do;
  630. return false;
  631. }
  632. if (!$this->image->loadFromBase64(base64_encode($avatarImage))) {
  633. return false;
  634. }
  635. // use the checksum before modifications
  636. $checksum = md5($this->image->data());
  637. if ($checksum === $this->config->getUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', '') && $this->avatarExists()) {
  638. return true;
  639. }
  640. $isSet = $this->setOwnCloudAvatar();
  641. if ($isSet) {
  642. // save checksum only after successful setting
  643. $this->config->setUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', $checksum);
  644. }
  645. return $isSet;
  646. }
  647. private function avatarExists(): bool {
  648. try {
  649. $currentAvatar = $this->avatarManager->getAvatar($this->uid);
  650. return $currentAvatar->exists() && $currentAvatar->isCustomAvatar();
  651. } catch (\Exception $e) {
  652. return false;
  653. }
  654. }
  655. /**
  656. * @brief sets an image as Nextcloud avatar
  657. * @return bool
  658. */
  659. private function setOwnCloudAvatar() {
  660. if (!$this->image->valid()) {
  661. $this->logger->error('avatar image data from LDAP invalid for ' . $this->dn, ['app' => 'user_ldap']);
  662. return false;
  663. }
  664. //make sure it is a square and not bigger than 512x512
  665. $size = min([$this->image->width(), $this->image->height(), 512]);
  666. if (!$this->image->centerCrop($size)) {
  667. $this->logger->error('croping image for avatar failed for ' . $this->dn, ['app' => 'user_ldap']);
  668. return false;
  669. }
  670. if (!$this->fs->isLoaded()) {
  671. $this->fs->setup($this->uid);
  672. }
  673. try {
  674. $avatar = $this->avatarManager->getAvatar($this->uid);
  675. $avatar->set($this->image);
  676. return true;
  677. } catch (\Exception $e) {
  678. $this->logger->info('Could not set avatar for ' . $this->dn, ['exception' => $e]);
  679. }
  680. return false;
  681. }
  682. /**
  683. * @throws AttributeNotSet
  684. * @throws \OC\ServerNotAvailableException
  685. * @throws PreConditionNotMetException
  686. */
  687. public function getExtStorageHome():string {
  688. $value = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', '');
  689. if ($value !== '') {
  690. return $value;
  691. }
  692. $value = $this->updateExtStorageHome();
  693. if ($value !== '') {
  694. return $value;
  695. }
  696. throw new AttributeNotSet(sprintf(
  697. 'external home storage attribute yield no value for %s', $this->getUsername()
  698. ));
  699. }
  700. /**
  701. * @throws PreConditionNotMetException
  702. * @throws \OC\ServerNotAvailableException
  703. */
  704. public function updateExtStorageHome(?string $valueFromLDAP = null):string {
  705. if ($valueFromLDAP === null) {
  706. $extHomeValues = $this->access->readAttribute($this->getDN(), $this->connection->ldapExtStorageHomeAttribute);
  707. } else {
  708. $extHomeValues = [$valueFromLDAP];
  709. }
  710. if ($extHomeValues && isset($extHomeValues[0])) {
  711. $extHome = $extHomeValues[0];
  712. $this->config->setUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', $extHome);
  713. return $extHome;
  714. } else {
  715. $this->config->deleteUserValue($this->getUsername(), 'user_ldap', 'extStorageHome');
  716. return '';
  717. }
  718. }
  719. /**
  720. * called by a post_login hook to handle password expiry
  721. *
  722. * @param array $params
  723. */
  724. public function handlePasswordExpiry($params) {
  725. $ppolicyDN = $this->connection->ldapDefaultPPolicyDN;
  726. if (empty($ppolicyDN) || ((int)$this->connection->turnOnPasswordChange !== 1)) {
  727. return;//password expiry handling disabled
  728. }
  729. $uid = $params['uid'];
  730. if (isset($uid) && $uid === $this->getUsername()) {
  731. //retrieve relevant user attributes
  732. $result = $this->access->search('objectclass=*', $this->dn, ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']);
  733. if (array_key_exists('pwdpolicysubentry', $result[0])) {
  734. $pwdPolicySubentry = $result[0]['pwdpolicysubentry'];
  735. if ($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)) {
  736. $ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN
  737. }
  738. }
  739. $pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : [];
  740. $pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : [];
  741. $pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : [];
  742. //retrieve relevant password policy attributes
  743. $cacheKey = 'ppolicyAttributes' . $ppolicyDN;
  744. $result = $this->connection->getFromCache($cacheKey);
  745. if (is_null($result)) {
  746. $result = $this->access->search('objectclass=*', $ppolicyDN, ['pwdgraceauthnlimit', 'pwdmaxage', 'pwdexpirewarning']);
  747. $this->connection->writeToCache($cacheKey, $result);
  748. }
  749. $pwdGraceAuthNLimit = array_key_exists('pwdgraceauthnlimit', $result[0]) ? $result[0]['pwdgraceauthnlimit'] : [];
  750. $pwdMaxAge = array_key_exists('pwdmaxage', $result[0]) ? $result[0]['pwdmaxage'] : [];
  751. $pwdExpireWarning = array_key_exists('pwdexpirewarning', $result[0]) ? $result[0]['pwdexpirewarning'] : [];
  752. //handle grace login
  753. if (!empty($pwdGraceUseTime)) { //was this a grace login?
  754. if (!empty($pwdGraceAuthNLimit)
  755. && count($pwdGraceUseTime) < (int)$pwdGraceAuthNLimit[0]) { //at least one more grace login available?
  756. $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true');
  757. header('Location: ' . \OC::$server->getURLGenerator()->linkToRouteAbsolute(
  758. 'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid]));
  759. } else { //no more grace login available
  760. header('Location: ' . \OC::$server->getURLGenerator()->linkToRouteAbsolute(
  761. 'user_ldap.renewPassword.showLoginFormInvalidPassword', ['user' => $uid]));
  762. }
  763. exit();
  764. }
  765. //handle pwdReset attribute
  766. if (!empty($pwdReset) && $pwdReset[0] === 'TRUE') { //user must change their password
  767. $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true');
  768. header('Location: ' . \OC::$server->getURLGenerator()->linkToRouteAbsolute(
  769. 'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid]));
  770. exit();
  771. }
  772. //handle password expiry warning
  773. if (!empty($pwdChangedTime)) {
  774. if (!empty($pwdMaxAge)
  775. && !empty($pwdExpireWarning)) {
  776. $pwdMaxAgeInt = (int)$pwdMaxAge[0];
  777. $pwdExpireWarningInt = (int)$pwdExpireWarning[0];
  778. if ($pwdMaxAgeInt > 0 && $pwdExpireWarningInt > 0) {
  779. $pwdChangedTimeDt = \DateTime::createFromFormat('YmdHisZ', $pwdChangedTime[0]);
  780. $pwdChangedTimeDt->add(new \DateInterval('PT' . $pwdMaxAgeInt . 'S'));
  781. $currentDateTime = new \DateTime();
  782. $secondsToExpiry = $pwdChangedTimeDt->getTimestamp() - $currentDateTime->getTimestamp();
  783. if ($secondsToExpiry <= $pwdExpireWarningInt) {
  784. //remove last password expiry warning if any
  785. $notification = $this->notificationManager->createNotification();
  786. $notification->setApp('user_ldap')
  787. ->setUser($uid)
  788. ->setObject('pwd_exp_warn', $uid)
  789. ;
  790. $this->notificationManager->markProcessed($notification);
  791. //create new password expiry warning
  792. $notification = $this->notificationManager->createNotification();
  793. $notification->setApp('user_ldap')
  794. ->setUser($uid)
  795. ->setDateTime($currentDateTime)
  796. ->setObject('pwd_exp_warn', $uid)
  797. ->setSubject('pwd_exp_warn_days', [(int)ceil($secondsToExpiry / 60 / 60 / 24)])
  798. ;
  799. $this->notificationManager->notify($notification);
  800. }
  801. }
  802. }
  803. }
  804. }
  805. }
  806. }