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

Principal.php 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. * @copyright Copyright (c) 2018, Georg Ehrke
  5. *
  6. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  7. * @author Bart Visscher <bartv@thisnet.nl>
  8. * @author Christoph Seitz <christoph.seitz@posteo.de>
  9. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  10. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  11. * @author Georg Ehrke <oc.list@georgehrke.com>
  12. * @author Jakob Sack <mail@jakobsack.de>
  13. * @author Julius Härtl <jus@bitgrid.net>
  14. * @author Lukas Reschke <lukas@statuscode.ch>
  15. * @author Morris Jobke <hey@morrisjobke.de>
  16. * @author Roeland Jago Douma <roeland@famdouma.nl>
  17. * @author Thomas Müller <thomas.mueller@tmit.eu>
  18. * @author Vincent Petry <vincent@nextcloud.com>
  19. * @author Vinicius Cubas Brand <vinicius@eita.org.br>
  20. *
  21. * @license AGPL-3.0
  22. *
  23. * This code is free software: you can redistribute it and/or modify
  24. * it under the terms of the GNU Affero General Public License, version 3,
  25. * as published by the Free Software Foundation.
  26. *
  27. * This program is distributed in the hope that it will be useful,
  28. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  29. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  30. * GNU Affero General Public License for more details.
  31. *
  32. * You should have received a copy of the GNU Affero General Public License, version 3,
  33. * along with this program. If not, see <http://www.gnu.org/licenses/>
  34. *
  35. */
  36. namespace OCA\DAV\Connector\Sabre;
  37. use OCA\Circles\Exceptions\CircleDoesNotExistException;
  38. use OCA\DAV\CalDAV\Proxy\ProxyMapper;
  39. use OCA\DAV\Traits\PrincipalProxyTrait;
  40. use OCP\App\IAppManager;
  41. use OCP\AppFramework\QueryException;
  42. use OCP\Constants;
  43. use OCP\IConfig;
  44. use OCP\IGroup;
  45. use OCP\IGroupManager;
  46. use OCP\IUser;
  47. use OCP\IUserManager;
  48. use OCP\IUserSession;
  49. use OCP\Share\IManager as IShareManager;
  50. use Sabre\DAV\Exception;
  51. use Sabre\DAV\PropPatch;
  52. use Sabre\DAVACL\PrincipalBackend\BackendInterface;
  53. class Principal implements BackendInterface {
  54. /** @var IUserManager */
  55. private $userManager;
  56. /** @var IGroupManager */
  57. private $groupManager;
  58. /** @var IShareManager */
  59. private $shareManager;
  60. /** @var IUserSession */
  61. private $userSession;
  62. /** @var IAppManager */
  63. private $appManager;
  64. /** @var string */
  65. private $principalPrefix;
  66. /** @var bool */
  67. private $hasGroups;
  68. /** @var bool */
  69. private $hasCircles;
  70. /** @var ProxyMapper */
  71. private $proxyMapper;
  72. /** @var IConfig */
  73. private $config;
  74. /**
  75. * Principal constructor.
  76. *
  77. * @param IUserManager $userManager
  78. * @param IGroupManager $groupManager
  79. * @param IShareManager $shareManager
  80. * @param IUserSession $userSession
  81. * @param IAppManager $appManager
  82. * @param ProxyMapper $proxyMapper
  83. * @param IConfig $config
  84. * @param string $principalPrefix
  85. */
  86. public function __construct(IUserManager $userManager,
  87. IGroupManager $groupManager,
  88. IShareManager $shareManager,
  89. IUserSession $userSession,
  90. IAppManager $appManager,
  91. ProxyMapper $proxyMapper,
  92. IConfig $config,
  93. string $principalPrefix = 'principals/users/') {
  94. $this->userManager = $userManager;
  95. $this->groupManager = $groupManager;
  96. $this->shareManager = $shareManager;
  97. $this->userSession = $userSession;
  98. $this->appManager = $appManager;
  99. $this->principalPrefix = trim($principalPrefix, '/');
  100. $this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
  101. $this->proxyMapper = $proxyMapper;
  102. $this->config = $config;
  103. }
  104. use PrincipalProxyTrait {
  105. getGroupMembership as protected traitGetGroupMembership;
  106. }
  107. /**
  108. * Returns a list of principals based on a prefix.
  109. *
  110. * This prefix will often contain something like 'principals'. You are only
  111. * expected to return principals that are in this base path.
  112. *
  113. * You are expected to return at least a 'uri' for every user, you can
  114. * return any additional properties if you wish so. Common properties are:
  115. * {DAV:}displayname
  116. *
  117. * @param string $prefixPath
  118. * @return string[]
  119. */
  120. public function getPrincipalsByPrefix($prefixPath) {
  121. $principals = [];
  122. if ($prefixPath === $this->principalPrefix) {
  123. foreach ($this->userManager->search('') as $user) {
  124. $principals[] = $this->userToPrincipal($user);
  125. }
  126. }
  127. return $principals;
  128. }
  129. /**
  130. * Returns a specific principal, specified by it's path.
  131. * The returned structure should be the exact same as from
  132. * getPrincipalsByPrefix.
  133. *
  134. * @param string $path
  135. * @return array
  136. */
  137. public function getPrincipalByPath($path) {
  138. [$prefix, $name] = \Sabre\Uri\split($path);
  139. $decodedName = urldecode($name);
  140. if ($name === 'calendar-proxy-write' || $name === 'calendar-proxy-read') {
  141. [$prefix2, $name2] = \Sabre\Uri\split($prefix);
  142. if ($prefix2 === $this->principalPrefix) {
  143. $user = $this->userManager->get($name2);
  144. if ($user !== null) {
  145. return [
  146. 'uri' => 'principals/users/' . $user->getUID() . '/' . $name,
  147. ];
  148. }
  149. return null;
  150. }
  151. }
  152. if ($prefix === $this->principalPrefix) {
  153. // Depending on where it is called, it may happen that this function
  154. // is called either with a urlencoded version of the name or with a non-urlencoded one.
  155. // The urldecode function replaces %## and +, both of which are forbidden in usernames.
  156. // Hence there can be no ambiguity here and it is safe to call urldecode on all usernames
  157. $user = $this->userManager->get($decodedName);
  158. if ($user !== null) {
  159. return $this->userToPrincipal($user);
  160. }
  161. } elseif ($prefix === 'principals/circles') {
  162. if ($this->userSession->getUser() !== null) {
  163. // At the time of writing - 2021-01-19 — a mixed state is possible.
  164. // The second condition can be removed when this is fixed.
  165. return $this->circleToPrincipal($decodedName)
  166. ?: $this->circleToPrincipal($name);
  167. }
  168. } elseif ($prefix === 'principals/groups') {
  169. // At the time of writing - 2021-01-19 — a mixed state is possible.
  170. // The second condition can be removed when this is fixed.
  171. $group = $this->groupManager->get($decodedName)
  172. ?: $this->groupManager->get($name);
  173. if ($group instanceof IGroup) {
  174. return [
  175. 'uri' => 'principals/groups/' . $name,
  176. '{DAV:}displayname' => $group->getDisplayName(),
  177. ];
  178. }
  179. }
  180. return null;
  181. }
  182. /**
  183. * Returns the list of groups a principal is a member of
  184. *
  185. * @param string $principal
  186. * @param bool $needGroups
  187. * @return array
  188. * @throws Exception
  189. */
  190. public function getGroupMembership($principal, $needGroups = false) {
  191. [$prefix, $name] = \Sabre\Uri\split($principal);
  192. if ($prefix !== $this->principalPrefix) {
  193. return [];
  194. }
  195. $user = $this->userManager->get($name);
  196. if (!$user) {
  197. throw new Exception('Principal not found');
  198. }
  199. $groups = [];
  200. if ($this->hasGroups || $needGroups) {
  201. $userGroups = $this->groupManager->getUserGroups($user);
  202. foreach ($userGroups as $userGroup) {
  203. $groups[] = 'principals/groups/' . urlencode($userGroup->getGID());
  204. }
  205. }
  206. $groups = array_unique(array_merge(
  207. $groups,
  208. $this->traitGetGroupMembership($principal, $needGroups)
  209. ));
  210. return $groups;
  211. }
  212. /**
  213. * @param string $path
  214. * @param PropPatch $propPatch
  215. * @return int
  216. */
  217. public function updatePrincipal($path, PropPatch $propPatch) {
  218. return 0;
  219. }
  220. /**
  221. * Search user principals
  222. *
  223. * @param array $searchProperties
  224. * @param string $test
  225. * @return array
  226. */
  227. protected function searchUserPrincipals(array $searchProperties, $test = 'allof') {
  228. $results = [];
  229. // If sharing is disabled, return the empty array
  230. $shareAPIEnabled = $this->shareManager->shareApiEnabled();
  231. if (!$shareAPIEnabled) {
  232. return [];
  233. }
  234. $allowEnumeration = $this->shareManager->allowEnumeration();
  235. $limitEnumeration = $this->shareManager->limitEnumerationToGroups();
  236. // If sharing is restricted to group members only,
  237. // return only members that have groups in common
  238. $restrictGroups = false;
  239. if ($this->shareManager->shareWithGroupMembersOnly()) {
  240. $user = $this->userSession->getUser();
  241. if (!$user) {
  242. return [];
  243. }
  244. $restrictGroups = $this->groupManager->getUserGroupIds($user);
  245. }
  246. $currentUserGroups = [];
  247. if ($limitEnumeration) {
  248. $currentUser = $this->userSession->getUser();
  249. if ($currentUser) {
  250. $currentUserGroups = $this->groupManager->getUserGroupIds($currentUser);
  251. }
  252. }
  253. $searchLimit = $this->config->getSystemValueInt('sharing.maxAutocompleteResults', Constants::SHARING_MAX_AUTOCOMPLETE_RESULTS_DEFAULT);
  254. if ($searchLimit <= 0) {
  255. $searchLimit = null;
  256. }
  257. foreach ($searchProperties as $prop => $value) {
  258. switch ($prop) {
  259. case '{http://sabredav.org/ns}email-address':
  260. $users = $this->userManager->getByEmail($value);
  261. if (!$allowEnumeration) {
  262. $users = \array_filter($users, static function (IUser $user) use ($value) {
  263. return $user->getEMailAddress() === $value;
  264. });
  265. }
  266. if ($limitEnumeration) {
  267. $users = \array_filter($users, function (IUser $user) use ($currentUserGroups, $value) {
  268. return !empty(array_intersect(
  269. $this->groupManager->getUserGroupIds($user),
  270. $currentUserGroups
  271. )) || $user->getEMailAddress() === $value;
  272. });
  273. }
  274. $results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
  275. // is sharing restricted to groups only?
  276. if ($restrictGroups !== false) {
  277. $userGroups = $this->groupManager->getUserGroupIds($user);
  278. if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
  279. return $carry;
  280. }
  281. }
  282. $carry[] = $this->principalPrefix . '/' . $user->getUID();
  283. return $carry;
  284. }, []);
  285. break;
  286. case '{DAV:}displayname':
  287. $users = $this->userManager->searchDisplayName($value, $searchLimit);
  288. if (!$allowEnumeration) {
  289. $users = \array_filter($users, static function (IUser $user) use ($value) {
  290. return $user->getDisplayName() === $value;
  291. });
  292. }
  293. if ($limitEnumeration) {
  294. $users = \array_filter($users, function (IUser $user) use ($currentUserGroups, $value) {
  295. return !empty(array_intersect(
  296. $this->groupManager->getUserGroupIds($user),
  297. $currentUserGroups
  298. )) || $user->getDisplayName() === $value;
  299. });
  300. }
  301. $results[] = array_reduce($users, function (array $carry, IUser $user) use ($restrictGroups) {
  302. // is sharing restricted to groups only?
  303. if ($restrictGroups !== false) {
  304. $userGroups = $this->groupManager->getUserGroupIds($user);
  305. if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
  306. return $carry;
  307. }
  308. }
  309. $carry[] = $this->principalPrefix . '/' . $user->getUID();
  310. return $carry;
  311. }, []);
  312. break;
  313. case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
  314. // If you add support for more search properties that qualify as a user-address,
  315. // please also add them to the array below
  316. $results[] = $this->searchUserPrincipals([
  317. // In theory this should also search for principal:principals/users/...
  318. // but that's used internally only anyway and i don't know of any client querying that
  319. '{http://sabredav.org/ns}email-address' => $value,
  320. ], 'anyof');
  321. break;
  322. default:
  323. $results[] = [];
  324. break;
  325. }
  326. }
  327. // results is an array of arrays, so this is not the first search result
  328. // but the results of the first searchProperty
  329. if (count($results) === 1) {
  330. return $results[0];
  331. }
  332. switch ($test) {
  333. case 'anyof':
  334. return array_values(array_unique(array_merge(...$results)));
  335. case 'allof':
  336. default:
  337. return array_values(array_intersect(...$results));
  338. }
  339. }
  340. /**
  341. * @param string $prefixPath
  342. * @param array $searchProperties
  343. * @param string $test
  344. * @return array
  345. */
  346. public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
  347. if (count($searchProperties) === 0) {
  348. return [];
  349. }
  350. switch ($prefixPath) {
  351. case 'principals/users':
  352. return $this->searchUserPrincipals($searchProperties, $test);
  353. default:
  354. return [];
  355. }
  356. }
  357. /**
  358. * @param string $uri
  359. * @param string $principalPrefix
  360. * @return string
  361. */
  362. public function findByUri($uri, $principalPrefix) {
  363. // If sharing is disabled, return the empty array
  364. $shareAPIEnabled = $this->shareManager->shareApiEnabled();
  365. if (!$shareAPIEnabled) {
  366. return null;
  367. }
  368. // If sharing is restricted to group members only,
  369. // return only members that have groups in common
  370. $restrictGroups = false;
  371. if ($this->shareManager->shareWithGroupMembersOnly()) {
  372. $user = $this->userSession->getUser();
  373. if (!$user) {
  374. return null;
  375. }
  376. $restrictGroups = $this->groupManager->getUserGroupIds($user);
  377. }
  378. if (strpos($uri, 'mailto:') === 0) {
  379. if ($principalPrefix === 'principals/users') {
  380. $users = $this->userManager->getByEmail(substr($uri, 7));
  381. if (count($users) !== 1) {
  382. return null;
  383. }
  384. $user = $users[0];
  385. if ($restrictGroups !== false) {
  386. $userGroups = $this->groupManager->getUserGroupIds($user);
  387. if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
  388. return null;
  389. }
  390. }
  391. return $this->principalPrefix . '/' . $user->getUID();
  392. }
  393. }
  394. if (substr($uri, 0, 10) === 'principal:') {
  395. $principal = substr($uri, 10);
  396. $principal = $this->getPrincipalByPath($principal);
  397. if ($principal !== null) {
  398. return $principal['uri'];
  399. }
  400. }
  401. return null;
  402. }
  403. /**
  404. * @param IUser $user
  405. * @return array
  406. */
  407. protected function userToPrincipal($user) {
  408. $userId = $user->getUID();
  409. $displayName = $user->getDisplayName();
  410. $principal = [
  411. 'uri' => $this->principalPrefix . '/' . $userId,
  412. '{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
  413. '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
  414. ];
  415. $email = $user->getEMailAddress();
  416. if (!empty($email)) {
  417. $principal['{http://sabredav.org/ns}email-address'] = $email;
  418. }
  419. return $principal;
  420. }
  421. public function getPrincipalPrefix() {
  422. return $this->principalPrefix;
  423. }
  424. /**
  425. * @param string $circleUniqueId
  426. * @return array|null
  427. */
  428. protected function circleToPrincipal($circleUniqueId) {
  429. if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
  430. return null;
  431. }
  432. try {
  433. $circle = \OCA\Circles\Api\v1\Circles::detailsCircle($circleUniqueId, true);
  434. } catch (QueryException $ex) {
  435. return null;
  436. } catch (CircleDoesNotExistException $ex) {
  437. return null;
  438. }
  439. if (!$circle) {
  440. return null;
  441. }
  442. $principal = [
  443. 'uri' => 'principals/circles/' . $circleUniqueId,
  444. '{DAV:}displayname' => $circle->getName(),
  445. ];
  446. return $principal;
  447. }
  448. /**
  449. * Returns the list of circles a principal is a member of
  450. *
  451. * @param string $principal
  452. * @return array
  453. * @throws Exception
  454. * @throws \OCP\AppFramework\QueryException
  455. * @suppress PhanUndeclaredClassMethod
  456. */
  457. public function getCircleMembership($principal):array {
  458. if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
  459. return [];
  460. }
  461. [$prefix, $name] = \Sabre\Uri\split($principal);
  462. if ($this->hasCircles && $prefix === $this->principalPrefix) {
  463. $user = $this->userManager->get($name);
  464. if (!$user) {
  465. throw new Exception('Principal not found');
  466. }
  467. $circles = \OCA\Circles\Api\v1\Circles::joinedCircles($name, true);
  468. $circles = array_map(function ($circle) {
  469. /** @var \OCA\Circles\Model\Circle $circle */
  470. return 'principals/circles/' . urlencode($circle->getUniqueId());
  471. }, $circles);
  472. return $circles;
  473. }
  474. return [];
  475. }
  476. }