BruteForceMiddlewareTest.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
  4. * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
  5. *
  6. * @license GNU AGPL version 3 or any later version
  7. *
  8. * This program is free software: you can redistribute it and/or modify
  9. * it under the terms of the GNU Affero General Public License as
  10. * published by the Free Software Foundation, either version 3 of the
  11. * License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. namespace Test\AppFramework\Middleware\Security;
  23. use OC\AppFramework\Middleware\Security\BruteForceMiddleware;
  24. use OC\AppFramework\Utility\ControllerMethodReflector;
  25. use OCP\AppFramework\Controller;
  26. use OCP\AppFramework\Http\Attribute\BruteForceProtection;
  27. use OCP\AppFramework\Http\Response;
  28. use OCP\IRequest;
  29. use OCP\Security\Bruteforce\IThrottler;
  30. use Psr\Log\LoggerInterface;
  31. use Test\TestCase;
  32. class TestController extends Controller {
  33. /**
  34. * @BruteForceProtection(action=login)
  35. */
  36. public function testMethodWithAnnotation() {
  37. }
  38. public function testMethodWithoutAnnotation() {
  39. }
  40. #[BruteForceProtection(action: 'single')]
  41. public function singleAttribute(): void {
  42. }
  43. #[BruteForceProtection(action: 'first')]
  44. #[BruteForceProtection(action: 'second')]
  45. public function multipleAttributes(): void {
  46. }
  47. }
  48. class BruteForceMiddlewareTest extends TestCase {
  49. /** @var ControllerMethodReflector */
  50. private $reflector;
  51. /** @var IThrottler|\PHPUnit\Framework\MockObject\MockObject */
  52. private $throttler;
  53. /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
  54. private $request;
  55. /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
  56. private $logger;
  57. private BruteForceMiddleware $bruteForceMiddleware;
  58. protected function setUp(): void {
  59. parent::setUp();
  60. $this->reflector = new ControllerMethodReflector();
  61. $this->throttler = $this->createMock(IThrottler::class);
  62. $this->request = $this->createMock(IRequest::class);
  63. $this->logger = $this->createMock(LoggerInterface::class);
  64. $this->bruteForceMiddleware = new BruteForceMiddleware(
  65. $this->reflector,
  66. $this->throttler,
  67. $this->request,
  68. $this->logger,
  69. );
  70. }
  71. public function testBeforeControllerWithAnnotation(): void {
  72. $this->request
  73. ->expects($this->once())
  74. ->method('getRemoteAddress')
  75. ->willReturn('127.0.0.1');
  76. $this->throttler
  77. ->expects($this->once())
  78. ->method('sleepDelayOrThrowOnMax')
  79. ->with('127.0.0.1', 'login');
  80. $controller = new TestController('test', $this->request);
  81. $this->reflector->reflect($controller, 'testMethodWithAnnotation');
  82. $this->bruteForceMiddleware->beforeController($controller, 'testMethodWithAnnotation');
  83. }
  84. public function testBeforeControllerWithSingleAttribute(): void {
  85. $this->request
  86. ->expects($this->once())
  87. ->method('getRemoteAddress')
  88. ->willReturn('::1');
  89. $this->throttler
  90. ->expects($this->once())
  91. ->method('sleepDelayOrThrowOnMax')
  92. ->with('::1', 'single');
  93. $controller = new TestController('test', $this->request);
  94. $this->reflector->reflect($controller, 'singleAttribute');
  95. $this->bruteForceMiddleware->beforeController($controller, 'singleAttribute');
  96. }
  97. public function testBeforeControllerWithMultipleAttributes(): void {
  98. $this->request
  99. ->expects($this->once())
  100. ->method('getRemoteAddress')
  101. ->willReturn('::1');
  102. $this->throttler
  103. ->expects($this->exactly(2))
  104. ->method('sleepDelayOrThrowOnMax')
  105. ->withConsecutive(
  106. ['::1', 'first'],
  107. ['::1', 'second'],
  108. );
  109. $controller = new TestController('test', $this->request);
  110. $this->reflector->reflect($controller, 'multipleAttributes');
  111. $this->bruteForceMiddleware->beforeController($controller, 'multipleAttributes');
  112. }
  113. public function testBeforeControllerWithoutAnnotation(): void {
  114. $this->request
  115. ->expects($this->never())
  116. ->method('getRemoteAddress');
  117. $this->throttler
  118. ->expects($this->never())
  119. ->method('sleepDelayOrThrowOnMax');
  120. $controller = new TestController('test', $this->request);
  121. $this->reflector->reflect($controller, 'testMethodWithoutAnnotation');
  122. $this->bruteForceMiddleware->beforeController($controller, 'testMethodWithoutAnnotation');
  123. }
  124. public function testAfterControllerWithAnnotationAndThrottledRequest(): void {
  125. /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
  126. $response = $this->createMock(Response::class);
  127. $response
  128. ->expects($this->once())
  129. ->method('isThrottled')
  130. ->willReturn(true);
  131. $response
  132. ->expects($this->once())
  133. ->method('getThrottleMetadata')
  134. ->willReturn([]);
  135. $this->request
  136. ->expects($this->once())
  137. ->method('getRemoteAddress')
  138. ->willReturn('127.0.0.1');
  139. $this->throttler
  140. ->expects($this->once())
  141. ->method('sleepDelayOrThrowOnMax')
  142. ->with('127.0.0.1', 'login');
  143. $this->throttler
  144. ->expects($this->once())
  145. ->method('registerAttempt')
  146. ->with('login', '127.0.0.1');
  147. $controller = new TestController('test', $this->request);
  148. $this->reflector->reflect($controller, 'testMethodWithAnnotation');
  149. $this->bruteForceMiddleware->afterController($controller, 'testMethodWithAnnotation', $response);
  150. }
  151. public function testAfterControllerWithAnnotationAndNotThrottledRequest(): void {
  152. /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
  153. $response = $this->createMock(Response::class);
  154. $response
  155. ->expects($this->once())
  156. ->method('isThrottled')
  157. ->willReturn(false);
  158. $this->request
  159. ->expects($this->never())
  160. ->method('getRemoteAddress');
  161. $this->throttler
  162. ->expects($this->never())
  163. ->method('sleepDelayOrThrowOnMax');
  164. $this->throttler
  165. ->expects($this->never())
  166. ->method('registerAttempt');
  167. $controller = new TestController('test', $this->request);
  168. $this->reflector->reflect($controller, 'testMethodWithAnnotation');
  169. $this->bruteForceMiddleware->afterController($controller, 'testMethodWithAnnotation', $response);
  170. }
  171. public function testAfterControllerWithSingleAttribute(): void {
  172. /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
  173. $response = $this->createMock(Response::class);
  174. $response
  175. ->expects($this->once())
  176. ->method('isThrottled')
  177. ->willReturn(true);
  178. $response
  179. ->expects($this->once())
  180. ->method('getThrottleMetadata')
  181. ->willReturn([]);
  182. $this->request
  183. ->expects($this->once())
  184. ->method('getRemoteAddress')
  185. ->willReturn('::1');
  186. $this->throttler
  187. ->expects($this->once())
  188. ->method('sleepDelayOrThrowOnMax')
  189. ->with('::1', 'single');
  190. $this->throttler
  191. ->expects($this->once())
  192. ->method('registerAttempt')
  193. ->with('single', '::1');
  194. $controller = new TestController('test', $this->request);
  195. $this->reflector->reflect($controller, 'singleAttribute');
  196. $this->bruteForceMiddleware->afterController($controller, 'singleAttribute', $response);
  197. }
  198. public function testAfterControllerWithMultipleAttributesGeneralMatch(): void {
  199. /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
  200. $response = $this->createMock(Response::class);
  201. $response
  202. ->expects($this->once())
  203. ->method('isThrottled')
  204. ->willReturn(true);
  205. $response
  206. ->expects($this->once())
  207. ->method('getThrottleMetadata')
  208. ->willReturn([]);
  209. $this->request
  210. ->expects($this->once())
  211. ->method('getRemoteAddress')
  212. ->willReturn('::1');
  213. $this->throttler
  214. ->expects($this->exactly(2))
  215. ->method('sleepDelayOrThrowOnMax')
  216. ->withConsecutive(
  217. ['::1', 'first'],
  218. ['::1', 'second'],
  219. );
  220. $this->throttler
  221. ->expects($this->exactly(2))
  222. ->method('registerAttempt')
  223. ->withConsecutive(
  224. ['first', '::1'],
  225. ['second', '::1'],
  226. );
  227. $controller = new TestController('test', $this->request);
  228. $this->reflector->reflect($controller, 'multipleAttributes');
  229. $this->bruteForceMiddleware->afterController($controller, 'multipleAttributes', $response);
  230. }
  231. public function testAfterControllerWithMultipleAttributesSpecificMatch(): void {
  232. /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
  233. $response = $this->createMock(Response::class);
  234. $response
  235. ->expects($this->once())
  236. ->method('isThrottled')
  237. ->willReturn(true);
  238. $response
  239. ->expects($this->once())
  240. ->method('getThrottleMetadata')
  241. ->willReturn(['action' => 'second']);
  242. $this->request
  243. ->expects($this->once())
  244. ->method('getRemoteAddress')
  245. ->willReturn('::1');
  246. $this->throttler
  247. ->expects($this->once())
  248. ->method('sleepDelayOrThrowOnMax')
  249. ->with('::1', 'second');
  250. $this->throttler
  251. ->expects($this->once())
  252. ->method('registerAttempt')
  253. ->with('second', '::1');
  254. $controller = new TestController('test', $this->request);
  255. $this->reflector->reflect($controller, 'multipleAttributes');
  256. $this->bruteForceMiddleware->afterController($controller, 'multipleAttributes', $response);
  257. }
  258. public function testAfterControllerWithoutAnnotation(): void {
  259. $this->request
  260. ->expects($this->never())
  261. ->method('getRemoteAddress');
  262. $this->throttler
  263. ->expects($this->never())
  264. ->method('sleepDelayOrThrowOnMax');
  265. $controller = new TestController('test', $this->request);
  266. $this->reflector->reflect($controller, 'testMethodWithoutAnnotation');
  267. /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
  268. $response = $this->createMock(Response::class);
  269. $this->bruteForceMiddleware->afterController($controller, 'testMethodWithoutAnnotation', $response);
  270. }
  271. public function testAfterControllerWithThrottledResponseButUnhandled(): void {
  272. $this->request
  273. ->expects($this->never())
  274. ->method('getRemoteAddress');
  275. $this->throttler
  276. ->expects($this->never())
  277. ->method('sleepDelayOrThrowOnMax');
  278. $controller = new TestController('test', $this->request);
  279. $this->reflector->reflect($controller, 'testMethodWithoutAnnotation');
  280. /** @var Response|\PHPUnit\Framework\MockObject\MockObject $response */
  281. $response = $this->createMock(Response::class);
  282. $response->method('isThrottled')
  283. ->willReturn(true);
  284. $this->logger->expects($this->once())
  285. ->method('debug')
  286. ->with('Response for Test\AppFramework\Middleware\Security\TestController::testMethodWithoutAnnotation got bruteforce throttled but has no annotation nor attribute defined.');
  287. $this->bruteForceMiddleware->afterController($controller, 'testMethodWithoutAnnotation', $response);
  288. }
  289. }