User.php 30 KB

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