231f80906e989ae03107d948a9c51815e90a77ea
[yaffs-website] / web / core / tests / Drupal / Tests / Core / Access / AccessManagerTest.php
1 <?php
2
3 /**
4  * @file
5  * Contains \Drupal\Tests\Core\Access\AccessManagerTest.
6  */
7
8 namespace Drupal\Tests\Core\Access;
9
10 use Drupal\Core\Access\AccessCheckInterface;
11 use Drupal\Core\Access\AccessException;
12 use Drupal\Core\Access\AccessResult;
13 use Drupal\Core\Access\CheckProvider;
14 use Drupal\Core\Cache\Context\CacheContextsManager;
15 use Drupal\Core\Routing\RouteMatch;
16 use Drupal\Core\Access\AccessManager;
17 use Drupal\Core\Access\DefaultAccessCheck;
18 use Drupal\Tests\UnitTestCase;
19 use Drupal\router_test\Access\DefinedTestAccessCheck;
20 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
21 use Symfony\Component\DependencyInjection\ContainerBuilder;
22 use Symfony\Component\Routing\Exception\RouteNotFoundException;
23 use Symfony\Component\Routing\Route;
24 use Symfony\Component\Routing\RouteCollection;
25
26 /**
27  * @coversDefaultClass \Drupal\Core\Access\AccessManager
28  * @group Access
29  */
30 class AccessManagerTest extends UnitTestCase {
31
32   /**
33    * The dependency injection container.
34    *
35    * @var \Symfony\Component\DependencyInjection\ContainerBuilder
36    */
37   protected $container;
38
39   /**
40    * The collection of routes, which are tested.
41    *
42    * @var \Symfony\Component\Routing\RouteCollection
43    */
44   protected $routeCollection;
45
46   /**
47    * The access manager to test.
48    *
49    * @var \Drupal\Core\Access\AccessManager
50    */
51   protected $accessManager;
52
53   /**
54    * The route provider.
55    *
56    * @var \PHPUnit_Framework_MockObject_MockObject
57    */
58   protected $routeProvider;
59
60   /**
61    * The parameter converter.
62    *
63    * @var \Drupal\Core\ParamConverter\ParamConverterManagerInterface|\PHPUnit_Framework_MockObject_MockObject
64    */
65   protected $paramConverter;
66
67   /**
68    * The mocked account.
69    *
70    * @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
71    */
72   protected $account;
73
74   /**
75    * The access arguments resolver.
76    *
77    * @var \Drupal\Core\Access\AccessArgumentsResolverFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
78    */
79   protected $argumentsResolverFactory;
80
81   /**
82    * @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
83    */
84   protected $currentUser;
85
86   /**
87    * @var \Drupal\Core\Access\CheckProvider
88    */
89   protected $checkProvider;
90
91   /**
92    * {@inheritdoc}
93    */
94   protected function setUp() {
95     parent::setUp();
96
97     $this->container = new ContainerBuilder();
98     $cache_contexts_manager = $this->prophesize(CacheContextsManager::class)->reveal();
99     $this->container->set('cache_contexts_manager', $cache_contexts_manager);
100     \Drupal::setContainer($this->container);
101
102     $this->routeCollection = new RouteCollection();
103     $this->routeCollection->add('test_route_1', new Route('/test-route-1'));
104     $this->routeCollection->add('test_route_2', new Route('/test-route-2', [], ['_access' => 'TRUE']));
105     $this->routeCollection->add('test_route_3', new Route('/test-route-3', [], ['_access' => 'FALSE']));
106     $this->routeCollection->add('test_route_4', new Route('/test-route-4/{value}', [], ['_access' => 'TRUE']));
107
108     $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
109     $map = [];
110     foreach ($this->routeCollection->all() as $name => $route) {
111       $map[] = [$name, [], $route];
112     }
113     $map[] = ['test_route_4', ['value' => 'example'], $this->routeCollection->get('test_route_4')];
114     $this->routeProvider->expects($this->any())
115       ->method('getRouteByName')
116       ->will($this->returnValueMap($map));
117
118     $map = [];
119     $map[] = ['test_route_1', [], '/test-route-1'];
120     $map[] = ['test_route_2', [], '/test-route-2'];
121     $map[] = ['test_route_3', [], '/test-route-3'];
122     $map[] = ['test_route_4', ['value' => 'example'], '/test-route-4/example'];
123
124     $this->paramConverter = $this->getMock('Drupal\Core\ParamConverter\ParamConverterManagerInterface');
125
126     $this->account = $this->getMock('Drupal\Core\Session\AccountInterface');
127     $this->currentUser = $this->getMock('Drupal\Core\Session\AccountInterface');
128     $this->argumentsResolverFactory = $this->getMock('Drupal\Core\Access\AccessArgumentsResolverFactoryInterface');
129     $this->checkProvider = new CheckProvider();
130     $this->checkProvider->setContainer($this->container);
131
132     $this->accessManager = new AccessManager($this->routeProvider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
133   }
134
135   /**
136    * Tests \Drupal\Core\Access\AccessManager::setChecks().
137    */
138   public function testSetChecks() {
139     // Check setChecks without any access checker defined yet.
140     $this->checkProvider->setChecks($this->routeCollection);
141
142     foreach ($this->routeCollection->all() as $route) {
143       $this->assertNull($route->getOption('_access_checks'));
144     }
145
146     $this->setupAccessChecker();
147
148     $this->checkProvider->setChecks($this->routeCollection);
149
150     $this->assertEquals($this->routeCollection->get('test_route_1')->getOption('_access_checks'), NULL);
151     $this->assertEquals($this->routeCollection->get('test_route_2')->getOption('_access_checks'), ['test_access_default']);
152     $this->assertEquals($this->routeCollection->get('test_route_3')->getOption('_access_checks'), ['test_access_default']);
153   }
154
155   /**
156    * Tests setChecks with a dynamic access checker.
157    */
158   public function testSetChecksWithDynamicAccessChecker() {
159     // Setup the access manager.
160     $this->accessManager = new AccessManager($this->routeProvider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
161
162     // Setup the dynamic access checker.
163     $access_check = $this->getMock('Drupal\Tests\Core\Access\TestAccessCheckInterface');
164     $this->container->set('test_access', $access_check);
165     $this->checkProvider->addCheckService('test_access', 'access');
166
167     $route = new Route('/test-path', [], ['_foo' => '1', '_bar' => '1']);
168     $route2 = new Route('/test-path', [], ['_foo' => '1', '_bar' => '2']);
169     $collection = new RouteCollection();
170     $collection->add('test_route', $route);
171     $collection->add('test_route2', $route2);
172
173     $access_check->expects($this->exactly(2))
174       ->method('applies')
175       ->with($this->isInstanceOf('Symfony\Component\Routing\Route'))
176       ->will($this->returnCallback(function (Route $route) {
177          return $route->getRequirement('_bar') == 2;
178       }));
179
180     $this->checkProvider->setChecks($collection);
181     $this->assertEmpty($route->getOption('_access_checks'));
182     $this->assertEquals(['test_access'], $route2->getOption('_access_checks'));
183   }
184
185   /**
186    * Tests \Drupal\Core\Access\AccessManager::check().
187    */
188   public function testCheck() {
189     $route_matches = [];
190
191     // Construct route match objects.
192     foreach ($this->routeCollection->all() as $route_name => $route) {
193       $route_matches[$route_name] = new RouteMatch($route_name, $route, [], []);
194     }
195
196     // Check route access without any access checker defined yet.
197     foreach ($route_matches as $route_match) {
198       $this->assertEquals(FALSE, $this->accessManager->check($route_match, $this->account));
199       $this->assertEquals(AccessResult::neutral(), $this->accessManager->check($route_match, $this->account, NULL, TRUE));
200     }
201
202     $this->setupAccessChecker();
203
204     // An access checker got setup, but the routes haven't been setup using
205     // setChecks.
206     foreach ($route_matches as $route_match) {
207       $this->assertEquals(FALSE, $this->accessManager->check($route_match, $this->account));
208       $this->assertEquals(AccessResult::neutral(), $this->accessManager->check($route_match, $this->account, NULL, TRUE));
209     }
210
211     // Now applicable access checks have been saved on each route object.
212     $this->checkProvider->setChecks($this->routeCollection);
213     $this->setupAccessArgumentsResolverFactory();
214
215     $this->assertEquals(FALSE, $this->accessManager->check($route_matches['test_route_1'], $this->account));
216     $this->assertEquals(TRUE, $this->accessManager->check($route_matches['test_route_2'], $this->account));
217     $this->assertEquals(FALSE, $this->accessManager->check($route_matches['test_route_3'], $this->account));
218     $this->assertEquals(TRUE, $this->accessManager->check($route_matches['test_route_4'], $this->account));
219     $this->assertEquals(AccessResult::neutral(), $this->accessManager->check($route_matches['test_route_1'], $this->account, NULL, TRUE));
220     $this->assertEquals(AccessResult::allowed(), $this->accessManager->check($route_matches['test_route_2'], $this->account, NULL, TRUE));
221     $this->assertEquals(AccessResult::forbidden(), $this->accessManager->check($route_matches['test_route_3'], $this->account, NULL, TRUE));
222     $this->assertEquals(AccessResult::allowed(), $this->accessManager->check($route_matches['test_route_4'], $this->account, NULL, TRUE));
223   }
224
225   /**
226    * Tests \Drupal\Core\Access\AccessManager::check() with no account specified.
227    *
228    * @covers ::check
229    */
230   public function testCheckWithNullAccount() {
231     $this->setupAccessChecker();
232     $this->checkProvider->setChecks($this->routeCollection);
233
234     $route = $this->routeCollection->get('test_route_2');
235     $route_match = new RouteMatch('test_route_2', $route, [], []);
236
237     // Asserts that the current user is passed to the access arguments resolver
238     // factory.
239     $this->setupAccessArgumentsResolverFactory()
240       ->with($route_match, $this->currentUser, NULL);
241
242     $this->assertTrue($this->accessManager->check($route_match));
243   }
244
245   /**
246    * Provides data for the conjunction test.
247    *
248    * @return array
249    *   An array of data for check conjunctions.
250    *
251    * @see \Drupal\Tests\Core\Access\AccessManagerTest::testCheckConjunctions()
252    */
253   public function providerTestCheckConjunctions() {
254     $access_allow = AccessResult::allowed();
255     $access_deny = AccessResult::neutral();
256     $access_kill = AccessResult::forbidden();
257
258     $access_configurations = [];
259     $access_configurations[] = [
260       'name' => 'test_route_4',
261       'condition_one' => 'TRUE',
262       'condition_two' => 'FALSE',
263       'expected' => $access_kill,
264     ];
265     $access_configurations[] = [
266       'name' => 'test_route_5',
267       'condition_one' => 'TRUE',
268       'condition_two' => 'NULL',
269       'expected' => $access_deny,
270     ];
271     $access_configurations[] = [
272       'name' => 'test_route_6',
273       'condition_one' => 'FALSE',
274       'condition_two' => 'NULL',
275       'expected' => $access_kill,
276     ];
277     $access_configurations[] = [
278       'name' => 'test_route_7',
279       'condition_one' => 'TRUE',
280       'condition_two' => 'TRUE',
281       'expected' => $access_allow,
282     ];
283     $access_configurations[] = [
284       'name' => 'test_route_8',
285       'condition_one' => 'FALSE',
286       'condition_two' => 'FALSE',
287       'expected' => $access_kill,
288     ];
289     $access_configurations[] = [
290       'name' => 'test_route_9',
291       'condition_one' => 'NULL',
292       'condition_two' => 'NULL',
293       'expected' => $access_deny,
294     ];
295
296     return $access_configurations;
297   }
298
299   /**
300    * Test \Drupal\Core\Access\AccessManager::check() with conjunctions.
301    *
302    * @dataProvider providerTestCheckConjunctions
303    */
304   public function testCheckConjunctions($name, $condition_one, $condition_two, $expected_access) {
305     $this->setupAccessChecker();
306     $access_check = new DefinedTestAccessCheck();
307     $this->container->register('test_access_defined', $access_check);
308     $this->checkProvider->addCheckService('test_access_defined', 'access', ['_test_access']);
309
310     $route_collection = new RouteCollection();
311     // Setup a test route for each access configuration.
312     $requirements = [
313       '_access' => $condition_one,
314       '_test_access' => $condition_two,
315     ];
316     $route = new Route($name, [], $requirements);
317     $route_collection->add($name, $route);
318
319     $this->checkProvider->setChecks($route_collection);
320     $this->setupAccessArgumentsResolverFactory();
321
322     $route_match = new RouteMatch($name, $route, [], []);
323     $this->assertEquals($expected_access->isAllowed(), $this->accessManager->check($route_match, $this->account));
324     $this->assertEquals($expected_access, $this->accessManager->check($route_match, $this->account, NULL, TRUE));
325   }
326
327   /**
328    * Tests the checkNamedRoute method.
329    *
330    * @see \Drupal\Core\Access\AccessManager::checkNamedRoute()
331    */
332   public function testCheckNamedRoute() {
333     $this->setupAccessChecker();
334     $this->checkProvider->setChecks($this->routeCollection);
335     $this->setupAccessArgumentsResolverFactory();
336
337     $this->paramConverter->expects($this->at(0))
338       ->method('convert')
339       ->with([RouteObjectInterface::ROUTE_NAME => 'test_route_2', RouteObjectInterface::ROUTE_OBJECT => $this->routeCollection->get('test_route_2')])
340       ->will($this->returnValue([]));
341     $this->paramConverter->expects($this->at(1))
342       ->method('convert')
343       ->with([RouteObjectInterface::ROUTE_NAME => 'test_route_2', RouteObjectInterface::ROUTE_OBJECT => $this->routeCollection->get('test_route_2')])
344       ->will($this->returnValue([]));
345
346     $this->paramConverter->expects($this->at(2))
347       ->method('convert')
348       ->with(['value' => 'example', RouteObjectInterface::ROUTE_NAME => 'test_route_4', RouteObjectInterface::ROUTE_OBJECT => $this->routeCollection->get('test_route_4')])
349       ->will($this->returnValue(['value' => 'example']));
350     $this->paramConverter->expects($this->at(3))
351       ->method('convert')
352       ->with(['value' => 'example', RouteObjectInterface::ROUTE_NAME => 'test_route_4', RouteObjectInterface::ROUTE_OBJECT => $this->routeCollection->get('test_route_4')])
353       ->will($this->returnValue(['value' => 'example']));
354
355     // Tests the access with routes with parameters without given request.
356     $this->assertEquals(TRUE, $this->accessManager->checkNamedRoute('test_route_2', [], $this->account));
357     $this->assertEquals(AccessResult::allowed(), $this->accessManager->checkNamedRoute('test_route_2', [], $this->account, TRUE));
358     $this->assertEquals(TRUE, $this->accessManager->checkNamedRoute('test_route_4', ['value' => 'example'], $this->account));
359     $this->assertEquals(AccessResult::allowed(), $this->accessManager->checkNamedRoute('test_route_4', ['value' => 'example'], $this->account, TRUE));
360   }
361
362   /**
363    * Tests the checkNamedRoute with upcasted values.
364    *
365    * @see \Drupal\Core\Access\AccessManager::checkNamedRoute()
366    */
367   public function testCheckNamedRouteWithUpcastedValues() {
368     $this->routeCollection = new RouteCollection();
369     $route = new Route('/test-route-1/{value}', [], ['_test_access' => 'TRUE']);
370     $this->routeCollection->add('test_route_1', $route);
371
372     $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
373     $this->routeProvider->expects($this->any())
374       ->method('getRouteByName')
375       ->with('test_route_1', ['value' => 'example'])
376       ->will($this->returnValue($route));
377
378     $map = [];
379     $map[] = ['test_route_1', ['value' => 'example'], '/test-route-1/example'];
380
381     $this->paramConverter = $this->getMock('Drupal\Core\ParamConverter\ParamConverterManagerInterface');
382     $this->paramConverter->expects($this->atLeastOnce())
383       ->method('convert')
384       ->with(['value' => 'example', RouteObjectInterface::ROUTE_NAME => 'test_route_1', RouteObjectInterface::ROUTE_OBJECT => $route])
385       ->will($this->returnValue(['value' => 'upcasted_value']));
386
387     $this->setupAccessArgumentsResolverFactory($this->exactly(2))
388       ->with($this->callback(function ($route_match) {
389         return $route_match->getParameters()->get('value') == 'upcasted_value';
390       }));
391
392     $this->accessManager = new AccessManager($this->routeProvider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
393
394     $access_check = $this->getMock('Drupal\Tests\Core\Access\TestAccessCheckInterface');
395     $access_check->expects($this->atLeastOnce())
396       ->method('applies')
397       ->will($this->returnValue(TRUE));
398     $access_check->expects($this->atLeastOnce())
399       ->method('access')
400       ->will($this->returnValue(AccessResult::forbidden()));
401
402     $this->container->set('test_access', $access_check);
403
404     $this->checkProvider->addCheckService('test_access', 'access');
405     $this->checkProvider->setChecks($this->routeCollection);
406
407     $this->assertEquals(FALSE, $this->accessManager->checkNamedRoute('test_route_1', ['value' => 'example'], $this->account));
408     $this->assertEquals(AccessResult::forbidden(), $this->accessManager->checkNamedRoute('test_route_1', ['value' => 'example'], $this->account, TRUE));
409   }
410
411   /**
412    * Tests the checkNamedRoute with default values.
413    *
414    * @covers ::checkNamedRoute
415    */
416   public function testCheckNamedRouteWithDefaultValue() {
417     $this->routeCollection = new RouteCollection();
418     $route = new Route('/test-route-1/{value}', ['value' => 'example'], ['_test_access' => 'TRUE']);
419     $this->routeCollection->add('test_route_1', $route);
420
421     $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
422     $this->routeProvider->expects($this->any())
423       ->method('getRouteByName')
424       ->with('test_route_1', [])
425       ->will($this->returnValue($route));
426
427     $map = [];
428     $map[] = ['test_route_1', ['value' => 'example'], '/test-route-1/example'];
429
430     $this->paramConverter = $this->getMock('Drupal\Core\ParamConverter\ParamConverterManagerInterface');
431     $this->paramConverter->expects($this->atLeastOnce())
432       ->method('convert')
433       ->with(['value' => 'example', RouteObjectInterface::ROUTE_NAME => 'test_route_1', RouteObjectInterface::ROUTE_OBJECT => $route])
434       ->will($this->returnValue(['value' => 'upcasted_value']));
435
436     $this->setupAccessArgumentsResolverFactory($this->exactly(2))
437       ->with($this->callback(function ($route_match) {
438         return $route_match->getParameters()->get('value') == 'upcasted_value';
439       }));
440
441     $this->accessManager = new AccessManager($this->routeProvider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
442
443     $access_check = $this->getMock('Drupal\Tests\Core\Access\TestAccessCheckInterface');
444     $access_check->expects($this->atLeastOnce())
445       ->method('applies')
446       ->will($this->returnValue(TRUE));
447     $access_check->expects($this->atLeastOnce())
448       ->method('access')
449       ->will($this->returnValue(AccessResult::forbidden()));
450
451     $this->container->set('test_access', $access_check);
452
453     $this->checkProvider->addCheckService('test_access', 'access');
454     $this->checkProvider->setChecks($this->routeCollection);
455
456     $this->assertEquals(FALSE, $this->accessManager->checkNamedRoute('test_route_1', [], $this->account));
457     $this->assertEquals(AccessResult::forbidden(), $this->accessManager->checkNamedRoute('test_route_1', [], $this->account, TRUE));
458   }
459
460   /**
461    * Tests checkNamedRoute given an invalid/non existing route name.
462    */
463   public function testCheckNamedRouteWithNonExistingRoute() {
464     $this->routeProvider->expects($this->any())
465       ->method('getRouteByName')
466       ->will($this->throwException(new RouteNotFoundException()));
467
468     $this->setupAccessChecker();
469
470     $this->assertEquals(FALSE, $this->accessManager->checkNamedRoute('test_route_1', [], $this->account), 'A non existing route lead to access.');
471     $this->assertEquals(AccessResult::forbidden()->addCacheTags(['config:core.extension']), $this->accessManager->checkNamedRoute('test_route_1', [], $this->account, TRUE), 'A non existing route lead to access.');
472   }
473
474   /**
475    * Tests that an access checker throws an exception for not allowed values.
476    *
477    * @dataProvider providerCheckException
478    */
479   public function testCheckException($return_value) {
480     $route_provider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
481
482     // Setup a test route for each access configuration.
483     $requirements = [
484       '_test_incorrect_value' => 'TRUE',
485     ];
486     $options = [
487       '_access_checks' => [
488         'test_incorrect_value',
489       ],
490     ];
491     $route = new Route('', [], $requirements, $options);
492
493     $route_provider->expects($this->any())
494       ->method('getRouteByName')
495       ->will($this->returnValue($route));
496
497     $this->paramConverter = $this->getMock('Drupal\Core\ParamConverter\ParamConverterManagerInterface');
498     $this->paramConverter->expects($this->any())
499       ->method('convert')
500       ->will($this->returnValue([]));
501
502     $this->setupAccessArgumentsResolverFactory();
503
504     $container = new ContainerBuilder();
505
506     // Register a service that will return an incorrect value.
507     $access_check = $this->getMock('Drupal\Tests\Core\Access\TestAccessCheckInterface');
508     $access_check->expects($this->any())
509       ->method('access')
510       ->will($this->returnValue($return_value));
511     $container->set('test_incorrect_value', $access_check);
512
513     $access_manager = new AccessManager($route_provider, $this->paramConverter, $this->argumentsResolverFactory, $this->currentUser, $this->checkProvider);
514     $this->checkProvider->setContainer($container);
515     $this->checkProvider->addCheckService('test_incorrect_value', 'access');
516
517     $this->setExpectedException(AccessException::class);
518     $access_manager->checkNamedRoute('test_incorrect_value', [], $this->account);
519   }
520
521   /**
522    * Data provider for testCheckException.
523    *
524    * @return array
525    */
526   public function providerCheckException() {
527     return [
528       [[1]],
529       ['string'],
530       [0],
531       [1],
532     ];
533   }
534
535   /**
536    * Adds a default access check service to the container and the access manager.
537    */
538   protected function setupAccessChecker() {
539     $access_check = new DefaultAccessCheck();
540     $this->container->register('test_access_default', $access_check);
541     $this->checkProvider->addCheckService('test_access_default', 'access', ['_access']);
542   }
543
544   /**
545    * Add default expectations to the access arguments resolver factory.
546    */
547   protected function setupAccessArgumentsResolverFactory($constraint = NULL) {
548     if (!isset($constraint)) {
549       $constraint = $this->any();
550     }
551     return $this->argumentsResolverFactory->expects($constraint)
552       ->method('getArgumentsResolver')
553       ->will($this->returnCallback(function ($route_match, $account) {
554         $resolver = $this->getMock('Drupal\Component\Utility\ArgumentsResolverInterface');
555         $resolver->expects($this->any())
556           ->method('getArguments')
557           ->will($this->returnCallback(function ($callable) use ($route_match) {
558             return [$route_match->getRouteObject()];
559           }));
560         return $resolver;
561       }));
562   }
563
564 }
565
566 /**
567  * Defines an interface with a defined access() method for mocking.
568  */
569 interface TestAccessCheckInterface extends AccessCheckInterface {
570   public function access();
571
572 }