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

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