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.

AbstractPrincipalBackend.php 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. <?php
  2. /**
  3. * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com>
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Georg Ehrke <oc.list@georgehrke.com>
  7. * @author Roeland Jago Douma <roeland@famdouma.nl>
  8. *
  9. * @license GNU AGPL version 3 or any later version
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU Affero General Public License as
  13. * published by the Free Software Foundation, either version 3 of the
  14. * License, or (at your option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU Affero General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Affero General Public License
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. *
  24. */
  25. namespace OCA\DAV\CalDAV\ResourceBooking;
  26. use OCA\DAV\CalDAV\Proxy\ProxyMapper;
  27. use OCA\DAV\Traits\PrincipalProxyTrait;
  28. use OCP\IDBConnection;
  29. use OCP\IGroupManager;
  30. use OCP\ILogger;
  31. use OCP\IUserSession;
  32. use Sabre\DAV\PropPatch;
  33. use Sabre\DAVACL\PrincipalBackend\BackendInterface;
  34. abstract class AbstractPrincipalBackend implements BackendInterface {
  35. /** @var IDBConnection */
  36. private $db;
  37. /** @var IUserSession */
  38. private $userSession;
  39. /** @var IGroupManager */
  40. private $groupManager;
  41. /** @var ILogger */
  42. private $logger;
  43. /** @var ProxyMapper */
  44. private $proxyMapper;
  45. /** @var string */
  46. private $principalPrefix;
  47. /** @var string */
  48. private $dbTableName;
  49. /** @var string */
  50. private $dbMetaDataTableName;
  51. /** @var string */
  52. private $dbForeignKeyName;
  53. /** @var string */
  54. private $cuType;
  55. /**
  56. * @param IDBConnection $dbConnection
  57. * @param IUserSession $userSession
  58. * @param IGroupManager $groupManager
  59. * @param ILogger $logger
  60. * @param string $principalPrefix
  61. * @param string $dbPrefix
  62. * @param string $cuType
  63. */
  64. public function __construct(IDBConnection $dbConnection,
  65. IUserSession $userSession,
  66. IGroupManager $groupManager,
  67. ILogger $logger,
  68. ProxyMapper $proxyMapper,
  69. string $principalPrefix,
  70. string $dbPrefix,
  71. string $cuType) {
  72. $this->db = $dbConnection;
  73. $this->userSession = $userSession;
  74. $this->groupManager = $groupManager;
  75. $this->logger = $logger;
  76. $this->proxyMapper = $proxyMapper;
  77. $this->principalPrefix = $principalPrefix;
  78. $this->dbTableName = 'calendar_' . $dbPrefix . 's';
  79. $this->dbMetaDataTableName = $this->dbTableName . '_md';
  80. $this->dbForeignKeyName = $dbPrefix . '_id';
  81. $this->cuType = $cuType;
  82. }
  83. use PrincipalProxyTrait;
  84. /**
  85. * Returns a list of principals based on a prefix.
  86. *
  87. * This prefix will often contain something like 'principals'. You are only
  88. * expected to return principals that are in this base path.
  89. *
  90. * You are expected to return at least a 'uri' for every user, you can
  91. * return any additional properties if you wish so. Common properties are:
  92. * {DAV:}displayname
  93. *
  94. * @param string $prefixPath
  95. * @return string[]
  96. */
  97. public function getPrincipalsByPrefix($prefixPath) {
  98. $principals = [];
  99. if ($prefixPath === $this->principalPrefix) {
  100. $query = $this->db->getQueryBuilder();
  101. $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname'])
  102. ->from($this->dbTableName);
  103. $stmt = $query->execute();
  104. $metaDataQuery = $this->db->getQueryBuilder();
  105. $metaDataQuery->select([$this->dbForeignKeyName, 'key', 'value'])
  106. ->from($this->dbMetaDataTableName);
  107. $metaDataStmt = $metaDataQuery->execute();
  108. $metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC);
  109. $metaDataById = [];
  110. foreach ($metaDataRows as $metaDataRow) {
  111. if (!isset($metaDataById[$metaDataRow[$this->dbForeignKeyName]])) {
  112. $metaDataById[$metaDataRow[$this->dbForeignKeyName]] = [];
  113. }
  114. $metaDataById[$metaDataRow[$this->dbForeignKeyName]][$metaDataRow['key']] =
  115. $metaDataRow['value'];
  116. }
  117. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  118. $id = $row['id'];
  119. if (isset($metaDataById[$id])) {
  120. $principals[] = $this->rowToPrincipal($row, $metaDataById[$id]);
  121. } else {
  122. $principals[] = $this->rowToPrincipal($row);
  123. }
  124. }
  125. $stmt->closeCursor();
  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. if (strpos($path, $this->principalPrefix) !== 0) {
  139. return null;
  140. }
  141. [, $name] = \Sabre\Uri\split($path);
  142. [$backendId, $resourceId] = explode('-', $name, 2);
  143. $query = $this->db->getQueryBuilder();
  144. $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname'])
  145. ->from($this->dbTableName)
  146. ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
  147. ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
  148. $stmt = $query->execute();
  149. $row = $stmt->fetch(\PDO::FETCH_ASSOC);
  150. if (!$row) {
  151. return null;
  152. }
  153. $metaDataQuery = $this->db->getQueryBuilder();
  154. $metaDataQuery->select(['key', 'value'])
  155. ->from($this->dbMetaDataTableName)
  156. ->where($metaDataQuery->expr()->eq($this->dbForeignKeyName, $metaDataQuery->createNamedParameter($row['id'])));
  157. $metaDataStmt = $metaDataQuery->execute();
  158. $metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC);
  159. $metadata = [];
  160. foreach ($metaDataRows as $metaDataRow) {
  161. $metadata[$metaDataRow['key']] = $metaDataRow['value'];
  162. }
  163. return $this->rowToPrincipal($row, $metadata);
  164. }
  165. /**
  166. * @param int $id
  167. * @return array|null
  168. */
  169. public function getPrincipalById($id):?array {
  170. $query = $this->db->getQueryBuilder();
  171. $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname'])
  172. ->from($this->dbTableName)
  173. ->where($query->expr()->eq('id', $query->createNamedParameter($id)));
  174. $stmt = $query->execute();
  175. $row = $stmt->fetch(\PDO::FETCH_ASSOC);
  176. if (!$row) {
  177. return null;
  178. }
  179. $metaDataQuery = $this->db->getQueryBuilder();
  180. $metaDataQuery->select(['key', 'value'])
  181. ->from($this->dbMetaDataTableName)
  182. ->where($metaDataQuery->expr()->eq($this->dbForeignKeyName, $metaDataQuery->createNamedParameter($row['id'])));
  183. $metaDataStmt = $metaDataQuery->execute();
  184. $metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC);
  185. $metadata = [];
  186. foreach ($metaDataRows as $metaDataRow) {
  187. $metadata[$metaDataRow['key']] = $metaDataRow['value'];
  188. }
  189. return $this->rowToPrincipal($row, $metadata);
  190. }
  191. /**
  192. * @param string $path
  193. * @param PropPatch $propPatch
  194. * @return int
  195. */
  196. public function updatePrincipal($path, PropPatch $propPatch) {
  197. return 0;
  198. }
  199. /**
  200. * @param string $prefixPath
  201. * @param array $searchProperties
  202. * @param string $test
  203. * @return array
  204. */
  205. public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
  206. $results = [];
  207. if (\count($searchProperties) === 0) {
  208. return [];
  209. }
  210. if ($prefixPath !== $this->principalPrefix) {
  211. return [];
  212. }
  213. $user = $this->userSession->getUser();
  214. if (!$user) {
  215. return [];
  216. }
  217. $usersGroups = $this->groupManager->getUserGroupIds($user);
  218. foreach ($searchProperties as $prop => $value) {
  219. switch ($prop) {
  220. case '{http://sabredav.org/ns}email-address':
  221. $query = $this->db->getQueryBuilder();
  222. $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
  223. ->from($this->dbTableName)
  224. ->where($query->expr()->iLike('email', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%')));
  225. $stmt = $query->execute();
  226. $principals = [];
  227. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  228. if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
  229. continue;
  230. }
  231. $principals[] = $this->rowToPrincipal($row)['uri'];
  232. }
  233. $results[] = $principals;
  234. $stmt->closeCursor();
  235. break;
  236. case '{DAV:}displayname':
  237. $query = $this->db->getQueryBuilder();
  238. $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
  239. ->from($this->dbTableName)
  240. ->where($query->expr()->iLike('displayname', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%')));
  241. $stmt = $query->execute();
  242. $principals = [];
  243. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  244. if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
  245. continue;
  246. }
  247. $principals[] = $this->rowToPrincipal($row)['uri'];
  248. }
  249. $results[] = $principals;
  250. $stmt->closeCursor();
  251. break;
  252. case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
  253. // If you add support for more search properties that qualify as a user-address,
  254. // please also add them to the array below
  255. $results[] = $this->searchPrincipals($this->principalPrefix, [
  256. '{http://sabredav.org/ns}email-address' => $value,
  257. ], 'anyof');
  258. break;
  259. default:
  260. $rowsByMetadata = $this->searchPrincipalsByMetadataKey($prop, $value);
  261. $filteredRows = array_filter($rowsByMetadata, function ($row) use ($usersGroups) {
  262. return $this->isAllowedToAccessResource($row, $usersGroups);
  263. });
  264. $results[] = array_map(function ($row): string {
  265. return $row['uri'];
  266. }, $filteredRows);
  267. break;
  268. }
  269. }
  270. // results is an array of arrays, so this is not the first search result
  271. // but the results of the first searchProperty
  272. if (count($results) === 1) {
  273. return $results[0];
  274. }
  275. switch ($test) {
  276. case 'anyof':
  277. return array_values(array_unique(array_merge(...$results)));
  278. case 'allof':
  279. default:
  280. return array_values(array_intersect(...$results));
  281. }
  282. }
  283. /**
  284. * Searches principals based on their metadata keys.
  285. * This allows to search for all principals with a specific key.
  286. * e.g.:
  287. * '{http://nextcloud.com/ns}room-building-address' => 'ABC Street 123, ...'
  288. *
  289. * @param $key
  290. * @param $value
  291. * @return array
  292. */
  293. private function searchPrincipalsByMetadataKey($key, $value):array {
  294. $query = $this->db->getQueryBuilder();
  295. $query->select([$this->dbForeignKeyName])
  296. ->from($this->dbMetaDataTableName)
  297. ->where($query->expr()->eq('key', $query->createNamedParameter($key)))
  298. ->andWhere($query->expr()->iLike('value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%')));
  299. $stmt = $query->execute();
  300. $rows = [];
  301. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  302. $id = $row[$this->dbForeignKeyName];
  303. $principalRow = $this->getPrincipalById($id);
  304. if (!$principalRow) {
  305. continue;
  306. }
  307. $rows[] = $principalRow;
  308. }
  309. return $rows;
  310. }
  311. /**
  312. * @param string $uri
  313. * @param string $principalPrefix
  314. * @return null|string
  315. */
  316. public function findByUri($uri, $principalPrefix) {
  317. $user = $this->userSession->getUser();
  318. if (!$user) {
  319. return null;
  320. }
  321. $usersGroups = $this->groupManager->getUserGroupIds($user);
  322. if (strpos($uri, 'mailto:') === 0) {
  323. $email = substr($uri, 7);
  324. $query = $this->db->getQueryBuilder();
  325. $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
  326. ->from($this->dbTableName)
  327. ->where($query->expr()->eq('email', $query->createNamedParameter($email)));
  328. $stmt = $query->execute();
  329. $row = $stmt->fetch(\PDO::FETCH_ASSOC);
  330. if (!$row) {
  331. return null;
  332. }
  333. if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
  334. return null;
  335. }
  336. return $this->rowToPrincipal($row)['uri'];
  337. }
  338. if (strpos($uri, 'principal:') === 0) {
  339. $path = substr($uri, 10);
  340. if (strpos($path, $this->principalPrefix) !== 0) {
  341. return null;
  342. }
  343. [, $name] = \Sabre\Uri\split($path);
  344. [$backendId, $resourceId] = explode('-', $name, 2);
  345. $query = $this->db->getQueryBuilder();
  346. $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
  347. ->from($this->dbTableName)
  348. ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
  349. ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
  350. $stmt = $query->execute();
  351. $row = $stmt->fetch(\PDO::FETCH_ASSOC);
  352. if (!$row) {
  353. return null;
  354. }
  355. if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
  356. return null;
  357. }
  358. return $this->rowToPrincipal($row)['uri'];
  359. }
  360. return null;
  361. }
  362. /**
  363. * convert database row to principal
  364. *
  365. * @param String[] $row
  366. * @param String[] $metadata
  367. * @return Array
  368. */
  369. private function rowToPrincipal(array $row, array $metadata = []):array {
  370. return array_merge([
  371. 'uri' => $this->principalPrefix . '/' . $row['backend_id'] . '-' . $row['resource_id'],
  372. '{DAV:}displayname' => $row['displayname'],
  373. '{http://sabredav.org/ns}email-address' => $row['email'],
  374. '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->cuType,
  375. ], $metadata);
  376. }
  377. /**
  378. * @param $row
  379. * @param $userGroups
  380. * @return bool
  381. */
  382. private function isAllowedToAccessResource(array $row, array $userGroups):bool {
  383. if (!isset($row['group_restrictions']) ||
  384. $row['group_restrictions'] === null ||
  385. $row['group_restrictions'] === '') {
  386. return true;
  387. }
  388. // group restrictions contains something, but not parsable, deny access and log warning
  389. $json = json_decode($row['group_restrictions']);
  390. if (!\is_array($json)) {
  391. $this->logger->info('group_restrictions field could not be parsed for ' . $this->dbTableName . '::' . $row['id'] . ', denying access to resource');
  392. return false;
  393. }
  394. // empty array => no group restrictions
  395. if (empty($json)) {
  396. return true;
  397. }
  398. return !empty(array_intersect($json, $userGroups));
  399. }
  400. }