PageRenderTime 76ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/concrete/src/Permission/IpAccessControlService.php

http://github.com/concrete5/concrete5
PHP | 479 lines | 277 code | 43 blank | 159 comment | 27 complexity | 82d26a1f2af53e962ad301a5f00beb80 MD5 | raw file
Possible License(s): MIT, LGPL-2.1, MPL-2.0-no-copyleft-exception, BSD-3-Clause
  1. <?php
  2. namespace Concrete\Core\Permission;
  3. use Concrete\Core\Entity\Permission\IpAccessControlCategory;
  4. use Concrete\Core\Entity\Permission\IpAccessControlEvent;
  5. use Concrete\Core\Entity\Permission\IpAccessControlRange;
  6. use Concrete\Core\Entity\Site\Site;
  7. use Concrete\Core\Logging\LoggerAwareInterface;
  8. use Concrete\Core\Logging\LoggerAwareTrait;
  9. use DateTime;
  10. use Doctrine\Common\Collections\Criteria;
  11. use Doctrine\ORM\EntityManagerInterface;
  12. use IPLib\Address\AddressInterface;
  13. use IPLib\Factory as IPFactory;
  14. use IPLib\Range\RangeInterface;
  15. class IpAccessControlService implements LoggerAwareInterface
  16. {
  17. use LoggerAwareTrait;
  18. /**
  19. * Bit mask for blacklist ranges.
  20. *
  21. * @var int
  22. */
  23. const IPRANGEFLAG_BLACKLIST = 0x0001;
  24. /**
  25. * Bit mask for whitelist ranges.
  26. *
  27. * @var int
  28. */
  29. const IPRANGEFLAG_WHITELIST = 0x0002;
  30. /**
  31. * Bit mask for manually generated ranges.
  32. *
  33. * @var int
  34. */
  35. const IPRANGEFLAG_MANUAL = 0x0010;
  36. /**
  37. * Bit mask for automatically generated ranges.
  38. *
  39. * @var int
  40. */
  41. const IPRANGEFLAG_AUTOMATIC = 0x0020;
  42. /**
  43. * IP range type: manually added to the blacklist.
  44. *
  45. * @var int
  46. */
  47. const IPRANGETYPE_BLACKLIST_MANUAL = 0x0011; // IPRANGEFLAG_BLACKLIST | IPRANGEFLAG_MANUAL
  48. /**
  49. * IP range type: automatically added to the blacklist.
  50. *
  51. * @var int
  52. */
  53. const IPRANGETYPE_BLACKLIST_AUTOMATIC = 0x0021; // IPRANGEFLAG_BLACKLIST | IPRANGEFLAG_AUTOMATIC
  54. /**
  55. * IP range type: manually added to the whitelist.
  56. *
  57. * @var int
  58. */
  59. const IPRANGETYPE_WHITELIST_MANUAL = 0x0012; // IPRANGEFLAG_WHITELIST | IPRANGEFLAG_MANUAL
  60. /**
  61. * @var \Doctrine\ORM\EntityManagerInterface
  62. */
  63. protected $em;
  64. /**
  65. * @var \Concrete\Core\Entity\Site\Site
  66. */
  67. protected $site;
  68. /**
  69. * The IP Access Control Category.
  70. *
  71. * @var \Concrete\Core\Entity\Permission\IpAccessControlCategory
  72. */
  73. protected $category;
  74. /**
  75. * @var \IPLib\Address\AddressInterface
  76. */
  77. protected $defaultIpAddress;
  78. /**
  79. * Initialize the instance.
  80. *
  81. * @param \Doctrine\ORM\EntityManagerInterface $em
  82. * @param \Concrete\Core\Entity\Site\Site $site
  83. * @param \Concrete\Core\Entity\Permission\IpAccessControlCategory $category
  84. * @param \IPLib\Address\AddressInterface $defaultIpAddress
  85. */
  86. public function __construct(EntityManagerInterface $em, Site $site, IpAccessControlCategory $category, AddressInterface $defaultIpAddress)
  87. {
  88. $this->em = $em;
  89. $this->site = $site;
  90. $this->category = $category;
  91. $this->defaultIpAddress = $defaultIpAddress;
  92. }
  93. /**
  94. * Get the IP Access Control Category.
  95. *
  96. * @return \Concrete\Core\Entity\Permission\IpAccessControlCategory
  97. */
  98. public function getCategory()
  99. {
  100. return $this->category;
  101. }
  102. /**
  103. * Check if an IP address is blacklisted.
  104. *
  105. * @param \IPLib\Address\AddressInterface|null $ipAddress
  106. *
  107. * @return bool
  108. */
  109. public function isBlacklisted(AddressInterface $ipAddress = null)
  110. {
  111. $range = $this->getRange($ipAddress);
  112. return $range !== null && ($range->getType() & self::IPRANGEFLAG_BLACKLIST);
  113. }
  114. /**
  115. * Check if an IP address is whitelisted.
  116. *
  117. * @param \IPLib\Address\AddressInterface|null $ipAddress
  118. *
  119. * @return bool
  120. */
  121. public function isWhitelisted(AddressInterface $ipAddress = null)
  122. {
  123. $range = $this->getRange($ipAddress);
  124. return $range !== null && ($range->getType() & self::IPRANGEFLAG_WHITELIST);
  125. }
  126. /**
  127. * Create and save an IP Access Control Event.
  128. *
  129. * @param \IPLib\Address\AddressInterface|null $ipAddress
  130. * @param bool $evenIfDisabled
  131. *
  132. * @return \Concrete\Core\Entity\Permission\IpAccessControlEvent|null
  133. */
  134. public function registerEvent(AddressInterface $ipAddress = null, $evenIfDisabled = false)
  135. {
  136. if (!$evenIfDisabled && !$this->getCategory()->isEnabled()) {
  137. return null;
  138. }
  139. $event = new IpAccessControlEvent();
  140. $event
  141. ->setCategory($this->getCategory())
  142. ->setSite($this->site)
  143. ->setIpAddress($ipAddress ?: $this->defaultIpAddress)
  144. ->setDateTime(new DateTime('now'))
  145. ;
  146. $this->em->persist($event);
  147. $this->em->flush($event);
  148. return $event;
  149. }
  150. /**
  151. * Check if the IP address has reached the threshold.
  152. *
  153. * @param \IPLib\Address\AddressInterface $ipAddress
  154. * @param bool $evenIfDisabled
  155. *
  156. * @return bool
  157. */
  158. public function isThresholdReached(AddressInterface $ipAddress = null, $evenIfDisabled = false)
  159. {
  160. if (!$evenIfDisabled && !$this->getCategory()->isEnabled()) {
  161. return false;
  162. }
  163. if ($this->isWhitelisted($ipAddress)) {
  164. return false;
  165. }
  166. if ($ipAddress === null) {
  167. $ipAddress = $this->defaultIpAddress;
  168. }
  169. $qb = $this->em->createQueryBuilder();
  170. $x = $qb->expr();
  171. $qb
  172. ->from(IpAccessControlEvent::class, 'e')
  173. ->select($x->count('e.ipAccessControlEventID'))
  174. ->where($x->eq('e.ip', ':ip'))
  175. ->andWhere($x->eq('e.category', ':category'))
  176. ->setParameter('ip', $ipAddress->getComparableString())
  177. ->setParameter('category', $this->getCategory()->getIpAccessControlCategoryID())
  178. ;
  179. if ($this->getCategory()->getTimeWindow() !== null) {
  180. $dateTimeLimit = new DateTime('-' . $this->getCategory()->getTimeWindow() . ' seconds');
  181. $qb
  182. ->andWhere($x->gt('e.dateTime', ':dateTimeLimit'))
  183. ->setParameter('dateTimeLimit', $dateTimeLimit)
  184. ;
  185. }
  186. if ($this->getCategory()->isSiteSpecific()) {
  187. $qb
  188. ->andWhere(
  189. $x->orX(
  190. $x->isNull('e.site'),
  191. $x->eq('e.site', ':site')
  192. )
  193. )
  194. ->setParameter('site', $this->site->getSiteID())
  195. ;
  196. }
  197. $numEvents = (int) $qb->getQuery()->getSingleScalarResult();
  198. return $numEvents >= $this->getCategory()->getMaxEvents();
  199. }
  200. /**
  201. * Add an IP address to the list of blacklisted IP address when too many events occur.
  202. *
  203. * @param \IPLib\Address\AddressInterface $ipAddress the IP to add to the blacklist (if null, we'll use the current IP address)
  204. * @param bool $evenIfDisabled if set to true, we'll add the IP address even if the IP ban system is disabled in the configuration
  205. *
  206. * @return \Concrete\Core\Entity\Permission\IpAccessControlRange|null
  207. */
  208. public function addToBlacklistForThresholdReached(AddressInterface $ipAddress = null, $evenIfDisabled = false)
  209. {
  210. if (!$evenIfDisabled && !$this->getCategory()->isEnabled()) {
  211. return null;
  212. }
  213. if ($ipAddress === null) {
  214. $ipAddress = $this->defaultIpAddress;
  215. }
  216. if ($this->getCategory()->getBanDuration() === null) {
  217. $banExpiration = null;
  218. } else {
  219. $banExpiration = new DateTime('+' . $this->getCategory()->getBanDuration() . ' minutes');
  220. }
  221. $range = $this->createRange(
  222. IPFactory::rangeFromBoundaries($ipAddress, $ipAddress),
  223. static::IPRANGETYPE_BLACKLIST_AUTOMATIC,
  224. $banExpiration
  225. );
  226. if ($this->getCategory()->getLogChannelHandle() !== '') {
  227. $this->logger->warning(
  228. t('IP address %1$s added to blacklist for the category %2$s.', $ipAddress->toString(), $this->getCategory()->getDisplayName()),
  229. [
  230. 'ip_address' => $ipAddress->toString(),
  231. 'category' => $this->getCategory()->getHandle(),
  232. ]
  233. );
  234. }
  235. return $range;
  236. }
  237. /**
  238. * Add persist an IP address range type.
  239. *
  240. * @param \IPLib\Range\RangeInterface $range the IP address range to persist
  241. * @param int $type The range type (one of the IpAccessControlService::IPRANGETYPE_... constants)
  242. * @param \DateTime $expiration The optional expiration of the range type
  243. *
  244. * @return \Concrete\Core\Entity\Permission\IpAccessControlRange
  245. */
  246. public function createRange(RangeInterface $range, $type, DateTime $expiration = null)
  247. {
  248. $result = new IpAccessControlRange();
  249. $result
  250. ->setCategory($this->getCategory())
  251. ->setIpRange($range)
  252. ->setType($type)
  253. ->setExpiration($expiration)
  254. ->setSite($this->site)
  255. ;
  256. $this->em->persist($result);
  257. $this->em->flush($result);
  258. return $result;
  259. }
  260. /**
  261. * Get the list of currently available ranges.
  262. *
  263. * @param int $type (one of the IPService::IPRANGETYPE_... constants)
  264. * @param bool $includeExpired Include expired records?
  265. *
  266. * @return \Doctrine\Common\Collections\Collection|\Concrete\Core\Entity\Permission\IpAccessControlRange[]
  267. */
  268. public function getRanges($type, $includeExpired = false)
  269. {
  270. $criteria = new Criteria();
  271. $x = $criteria->expr();
  272. $criteria->andWhere($x->eq('type', (int) $type));
  273. if (!$includeExpired) {
  274. $criteria->andWhere(
  275. $x->orX(
  276. $x->isNull('expiration'),
  277. $x->gt('expiration', new DateTime('now'))
  278. )
  279. );
  280. }
  281. return $this->getCategory()->getRanges()->matching($criteria);
  282. }
  283. /**
  284. * Get a saved range for this category given its ID.
  285. *
  286. * @param int $id
  287. *
  288. * \Concrete\Core\Entity\Permission\IpAccessControlRange|null
  289. */
  290. public function getRangeByID($id)
  291. {
  292. if (!$id) {
  293. return null;
  294. }
  295. $entity = $this->em->find(IpAccessControlRange::class, ['ipAccessControlRangeID' => (int) $id]);
  296. if ($entity === null) {
  297. return null;
  298. }
  299. if ($entity->getCategory() !== $this->getCategory()) {
  300. return null;
  301. }
  302. return $entity;
  303. }
  304. /**
  305. * Delete a saved range given its instance or its ID.
  306. *
  307. * @param \Concrete\Core\Entity\Permission\IpAccessControlRange|int $range
  308. */
  309. public function deleteRange($range)
  310. {
  311. $entity = is_numeric($range) ? $this->getRangeByID($range) : $range;
  312. if (!($entity instanceof IpAccessControlRange) || $entity->getCategory() !== $this->getCategory()) {
  313. return;
  314. }
  315. $this->em->remove($entity);
  316. $this->em->flush($entity);
  317. }
  318. /**
  319. * Delete the recorded events.
  320. *
  321. * @param int|null $minAge the minimum age (in seconds) of the records (specify an empty value to delete all records)
  322. *
  323. * @return int the number of records deleted
  324. */
  325. public function deleteEvents($minAge = null)
  326. {
  327. $qb = $this->em->createQueryBuilder();
  328. $x = $qb->expr();
  329. $qb
  330. ->delete(IpAccessControlEvent::class, 'e')
  331. ->where($x->eq('e.category', ':category'))
  332. ->setParameter('category', $this->getCategory()->getIpAccessControlCategoryID())
  333. ;
  334. if ($minAge) {
  335. $dateTimeLimit = new DateTime('-' . ((int) $minAge) . ' seconds');
  336. $qb
  337. ->andWhere($x->lte('e.dateTime', ':dateTimeLimit'))
  338. ->setParameter('dateTimeLimit', $dateTimeLimit)
  339. ;
  340. }
  341. return (int) $qb->getQuery()->execute();
  342. }
  343. /**
  344. * Clear the IP addresses automatically blacklisted.
  345. *
  346. * @param bool $onlyExpired
  347. *
  348. * @return int the number of records deleted
  349. */
  350. public function deleteAutomaticBlacklist($onlyExpired = true)
  351. {
  352. $qb = $this->em->createQueryBuilder();
  353. $x = $qb->expr();
  354. $qb
  355. ->delete(IpAccessControlRange::class, 'r')
  356. ->where($x->eq('r.category', ':category'))
  357. ->andWhere($x->eq('r.type', ':type'))
  358. ->setParameter('category', $this->getCategory()->getIpAccessControlCategoryID())
  359. ->setParameter('type', self::IPRANGETYPE_BLACKLIST_AUTOMATIC)
  360. ;
  361. if ($onlyExpired) {
  362. $dateTimeLimit = new DateTime('now');
  363. $qb
  364. ->andWhere($x->lte('r.expiration', ':dateTimeLimit'))
  365. ->setParameter('dateTimeLimit', $dateTimeLimit)
  366. ;
  367. }
  368. return (int) $qb->getQuery()->execute();
  369. }
  370. /**
  371. * Get the (localized) message telling the users that their IP address has been banned.
  372. *
  373. * @return string
  374. */
  375. public function getErrorMessage()
  376. {
  377. return t('Unable to complete action: your IP address has been banned. Please contact the administrator of this site for more information.');
  378. }
  379. /**
  380. * {@inheritdoc}
  381. *
  382. * @see \Concrete\Core\Logging\LoggerAwareInterface::getLoggerChannel()
  383. */
  384. public function getLoggerChannel()
  385. {
  386. return $this->getCategory()->getLogChannelHandle();
  387. }
  388. /**
  389. * @param \IPLib\Address\AddressInterface $ipAddress
  390. *
  391. * @return \Concrete\Core\Entity\Permission\IpAccessControlRange|null
  392. */
  393. public function getRange(AddressInterface $ipAddress = null)
  394. {
  395. if ($ipAddress === null) {
  396. $ipAddress = $this->defaultIpAddress;
  397. }
  398. $qb = $this->em->createQueryBuilder();
  399. $x = $qb->expr();
  400. $qb
  401. ->from(IpAccessControlRange::class, 'r')
  402. ->select('r')
  403. ->where($x->lte('r.ipFrom', ':ip'))
  404. ->andWhere($x->gte('r.ipTo', ':ip'))
  405. ->andWhere(
  406. $x->orX(
  407. $x->isNull('r.expiration'),
  408. $x->gt('r.expiration', ':now')
  409. )
  410. )
  411. ->setParameter('ip', $ipAddress->getComparableString())
  412. ->setParameter('now', new DateTime('now'))
  413. ;
  414. if ($this->getCategory()->isSiteSpecific()) {
  415. $qb
  416. ->andWhere(
  417. $x->orX(
  418. $x->isNull('r.site'),
  419. $x->eq('r.site', ':site')
  420. )
  421. )
  422. ->setParameter('site', $this->site->getSiteID())
  423. ;
  424. }
  425. $query = $qb->getQuery();
  426. $result = null;
  427. foreach ($query->getResult() as $range) {
  428. $result = $range;
  429. if ($range->getType() & self::IPRANGEFLAG_WHITELIST) {
  430. break;
  431. }
  432. }
  433. return $result;
  434. }
  435. }