AbstractPrincipalBackend.php 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. <?php
  2. /**
  3. * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
  4. *
  5. * @author Georg Ehrke <oc.list@georgehrke.com>
  6. *
  7. * @license GNU AGPL version 3 or any later version
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as
  11. * published by the Free Software Foundation, either version 3 of the
  12. * License, or (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. */
  23. namespace OCA\DAV\CalDAV\ResourceBooking;
  24. use OCP\IDBConnection;
  25. use OCP\IGroupManager;
  26. use OCP\ILogger;
  27. use OCP\IUserSession;
  28. use Sabre\DAVACL\PrincipalBackend\BackendInterface;
  29. use Sabre\DAV\Exception;
  30. use \Sabre\DAV\PropPatch;
  31. abstract class AbstractPrincipalBackend implements BackendInterface {
  32. /** @var IDBConnection */
  33. private $db;
  34. /** @var IUserSession */
  35. private $userSession;
  36. /** @var IGroupManager */
  37. private $groupManager;
  38. /** @var ILogger */
  39. private $logger;
  40. /** @var string */
  41. private $principalPrefix;
  42. /** @var string */
  43. private $dbTableName;
  44. /** @var string */
  45. private $cuType;
  46. /**
  47. * @param IDBConnection $dbConnection
  48. * @param IUserSession $userSession
  49. * @param IGroupManager $groupManager
  50. * @param ILogger $logger
  51. * @param string $principalPrefix
  52. * @param string $dbPrefix
  53. * @param string $cuType
  54. */
  55. public function __construct(IDBConnection $dbConnection,
  56. IUserSession $userSession,
  57. IGroupManager $groupManager,
  58. ILogger $logger,
  59. string $principalPrefix,
  60. string $dbPrefix,
  61. string $cuType) {
  62. $this->db = $dbConnection;
  63. $this->userSession = $userSession;
  64. $this->groupManager = $groupManager;
  65. $this->logger = $logger;
  66. $this->principalPrefix = $principalPrefix;
  67. $this->dbTableName = 'calendar_' . $dbPrefix;
  68. $this->cuType = $cuType;
  69. }
  70. /**
  71. * Returns a list of principals based on a prefix.
  72. *
  73. * This prefix will often contain something like 'principals'. You are only
  74. * expected to return principals that are in this base path.
  75. *
  76. * You are expected to return at least a 'uri' for every user, you can
  77. * return any additional properties if you wish so. Common properties are:
  78. * {DAV:}displayname
  79. *
  80. * @param string $prefixPath
  81. * @return string[]
  82. */
  83. public function getPrincipalsByPrefix($prefixPath) {
  84. $principals = [];
  85. if ($prefixPath === $this->principalPrefix) {
  86. $query = $this->db->getQueryBuilder();
  87. $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname'])
  88. ->from($this->dbTableName);
  89. $stmt = $query->execute();
  90. while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  91. $principals[] = $this->rowToPrincipal($row);
  92. }
  93. $stmt->closeCursor();
  94. }
  95. return $principals;
  96. }
  97. /**
  98. * Returns a specific principal, specified by it's path.
  99. * The returned structure should be the exact same as from
  100. * getPrincipalsByPrefix.
  101. *
  102. * @param string $path
  103. * @return array
  104. */
  105. public function getPrincipalByPath($path) {
  106. if (strpos($path, $this->principalPrefix) !== 0) {
  107. return null;
  108. }
  109. list(, $name) = \Sabre\Uri\split($path);
  110. list($backendId, $resourceId) = explode('-', $name, 2);
  111. $query = $this->db->getQueryBuilder();
  112. $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname'])
  113. ->from($this->dbTableName)
  114. ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
  115. ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
  116. $stmt = $query->execute();
  117. $row = $stmt->fetch(\PDO::FETCH_ASSOC);
  118. if(!$row) {
  119. return null;
  120. }
  121. return $this->rowToPrincipal($row);
  122. }
  123. /**
  124. * Returns the list of members for a group-principal
  125. *
  126. * @param string $principal
  127. * @return string[]
  128. */
  129. public function getGroupMemberSet($principal) {
  130. return [];
  131. }
  132. /**
  133. * Returns the list of groups a principal is a member of
  134. *
  135. * @param string $principal
  136. * @return array
  137. */
  138. public function getGroupMembership($principal) {
  139. return [];
  140. }
  141. /**
  142. * Updates the list of group members for a group principal.
  143. *
  144. * The principals should be passed as a list of uri's.
  145. *
  146. * @param string $principal
  147. * @param string[] $members
  148. * @throws Exception
  149. */
  150. public function setGroupMemberSet($principal, array $members) {
  151. throw new Exception('Setting members of the group is not supported yet');
  152. }
  153. /**
  154. * @param string $path
  155. * @param PropPatch $propPatch
  156. * @return int
  157. */
  158. function updatePrincipal($path, PropPatch $propPatch) {
  159. return 0;
  160. }
  161. /**
  162. * @param string $prefixPath
  163. * @param array $searchProperties
  164. * @param string $test
  165. * @return array
  166. */
  167. function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
  168. $results = [];
  169. if (\count($searchProperties) === 0) {
  170. return [];
  171. }
  172. if ($prefixPath !== $this->principalPrefix) {
  173. return [];
  174. }
  175. $user = $this->userSession->getUser();
  176. if (!$user) {
  177. return [];
  178. }
  179. $usersGroups = $this->groupManager->getUserGroupIds($user);
  180. foreach ($searchProperties as $prop => $value) {
  181. switch ($prop) {
  182. case '{http://sabredav.org/ns}email-address':
  183. $query = $this->db->getQueryBuilder();
  184. $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
  185. ->from($this->dbTableName)
  186. ->where($query->expr()->iLike('email', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%')));
  187. $stmt = $query->execute();
  188. $principals = [];
  189. while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  190. if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
  191. continue;
  192. }
  193. $principals[] = $this->rowToPrincipal($row)['uri'];
  194. }
  195. $results[] = $principals;
  196. $stmt->closeCursor();
  197. break;
  198. case '{DAV:}displayname':
  199. $query = $this->db->getQueryBuilder();
  200. $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
  201. ->from($this->dbTableName)
  202. ->where($query->expr()->iLike('displayname', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%')));
  203. $stmt = $query->execute();
  204. $principals = [];
  205. while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  206. if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
  207. continue;
  208. }
  209. $principals[] = $this->rowToPrincipal($row)['uri'];
  210. }
  211. $results[] = $principals;
  212. $stmt->closeCursor();
  213. break;
  214. default:
  215. $results[] = [];
  216. break;
  217. }
  218. }
  219. // results is an array of arrays, so this is not the first search result
  220. // but the results of the first searchProperty
  221. if (count($results) === 1) {
  222. return $results[0];
  223. }
  224. switch ($test) {
  225. case 'anyof':
  226. return array_values(array_unique(array_merge(...$results)));
  227. case 'allof':
  228. default:
  229. return array_values(array_intersect(...$results));
  230. }
  231. }
  232. /**
  233. * @param string $uri
  234. * @param string $principalPrefix
  235. * @return null|string
  236. */
  237. function findByUri($uri, $principalPrefix) {
  238. $user = $this->userSession->getUser();
  239. if (!$user) {
  240. return null;
  241. }
  242. $usersGroups = $this->groupManager->getUserGroupIds($user);
  243. if (strpos($uri, 'mailto:') === 0) {
  244. $email = substr($uri, 7);
  245. $query = $this->db->getQueryBuilder();
  246. $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
  247. ->from($this->dbTableName)
  248. ->where($query->expr()->eq('email', $query->createNamedParameter($email)));
  249. $stmt = $query->execute();
  250. $row = $stmt->fetch(\PDO::FETCH_ASSOC);
  251. if(!$row) {
  252. return null;
  253. }
  254. if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
  255. return null;
  256. }
  257. return $this->rowToPrincipal($row)['uri'];
  258. }
  259. if (strpos($uri, 'principal:') === 0) {
  260. $path = substr($uri, 10);
  261. if (strpos($path, $this->principalPrefix) !== 0) {
  262. return null;
  263. }
  264. list(, $name) = \Sabre\Uri\split($path);
  265. list($backendId, $resourceId) = explode('-', $name, 2);
  266. $query = $this->db->getQueryBuilder();
  267. $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
  268. ->from($this->dbTableName)
  269. ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
  270. ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
  271. $stmt = $query->execute();
  272. $row = $stmt->fetch(\PDO::FETCH_ASSOC);
  273. if(!$row) {
  274. return null;
  275. }
  276. if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
  277. return null;
  278. }
  279. return $this->rowToPrincipal($row)['uri'];
  280. }
  281. return null;
  282. }
  283. /**
  284. * convert database row to principal
  285. */
  286. private function rowToPrincipal($row) {
  287. return [
  288. 'uri' => $this->principalPrefix . '/' . $row['backend_id'] . '-' . $row['resource_id'],
  289. '{DAV:}displayname' => $row['displayname'],
  290. '{http://sabredav.org/ns}email-address' => $row['email'],
  291. '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->cuType,
  292. ];
  293. }
  294. /**
  295. * @param $row
  296. * @param $userGroups
  297. * @return bool
  298. */
  299. private function isAllowedToAccessResource($row, $userGroups) {
  300. if (!isset($row['group_restrictions']) ||
  301. $row['group_restrictions'] === null ||
  302. $row['group_restrictions'] === '') {
  303. return true;
  304. }
  305. // group restrictions contains something, but not parsable, deny access and log warning
  306. $json = json_decode($row['group_restrictions']);
  307. if (!\is_array($json)) {
  308. $this->logger->info('group_restrictions field could not be parsed for ' . $this->dbTableName . '::' . $row['id'] . ', denying access to resource');
  309. return false;
  310. }
  311. // empty array => no group restrictions
  312. if (empty($json)) {
  313. return true;
  314. }
  315. return !empty(array_intersect($json, $userGroups));
  316. }
  317. }