Skip to content

Commit eb9a945

Browse files
authored
Merge pull request #19 from SimLibaud/feature/js-client
Add Javascript module which allows to generate links depending on user routes access rules
2 parents 05c8b6e + 5e9dc02 commit eb9a945

15 files changed

Lines changed: 431 additions & 5 deletions

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
composer.lock
22
vendor/*
3-
3+
node_modules/*
4+
yarn.lock
5+
yarn-error.log
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Sil\RouteSecurityBundle\Controller;
5+
6+
use Sil\RouteSecurityBundle\Exception\LogicException;
7+
use Sil\RouteSecurityBundle\Security\AccessControl;
8+
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
9+
use Symfony\Component\HttpFoundation\Response;
10+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
11+
use Symfony\Component\Security\Core\User\UserInterface;
12+
use Symfony\Contracts\Cache\ItemInterface;
13+
use Twig\Environment;
14+
15+
class ExportJsSecuredRoutesController
16+
{
17+
/** @var AccessControl */
18+
private $accessControl;
19+
20+
/** @var TokenStorageInterface */
21+
private $tokenStorage;
22+
23+
/** @var Environment */
24+
private $twig;
25+
26+
/** @var string */
27+
private $cacheDir;
28+
29+
/**
30+
* @param AccessControl $accessControl
31+
* @param TokenStorageInterface $tokenStorage
32+
* @param Environment $twig
33+
* @param string $cacheDir
34+
*/
35+
public function __construct(AccessControl $accessControl, TokenStorageInterface $tokenStorage, Environment $twig, string $cacheDir)
36+
{
37+
$this->accessControl = $accessControl;
38+
$this->tokenStorage = $tokenStorage;
39+
$this->twig = $twig;
40+
$this->cacheDir = $cacheDir;
41+
}
42+
43+
/**
44+
* @return Response
45+
* @throws \Psr\Cache\InvalidArgumentException
46+
*/
47+
public function exportAction()
48+
{
49+
if (null === $this->tokenStorage->getToken()) {
50+
throw new LogicException('Unable to retrive the current user. The token storage does not contain security token.');
51+
}
52+
53+
if (false === $this->tokenStorage->getToken()->getUser() instanceof UserInterface) {
54+
throw new LogicException(sprintf('The security token must containt an User object that implements %s', UserInterface::class));
55+
}
56+
57+
$user = $this->tokenStorage->getToken()->getUser();
58+
59+
$cacheKey = md5($user->getUsername().json_encode($user->getRoles()));
60+
61+
$cache = new FilesystemAdapter('sil_route_security_bundle', 0, $this->cacheDir);
62+
63+
$securedRoutesWithUserPermission = $cache->get($cacheKey, function (ItemInterface $item) use ($user){
64+
$item->expiresAfter(3600);
65+
66+
$securedRoutesWithUserPermission = [];
67+
foreach ($this->accessControl->getAllSecuredRoutes() as $route) {
68+
$securedRoutesWithUserPermission[$route] = $this->accessControl->hasUserAccessToRoute($user, $route);
69+
}
70+
71+
return $securedRoutesWithUserPermission;
72+
});
73+
74+
return new Response($this->twig->render(
75+
'@SilRouteSecurity/secured_routes.js.twig',
76+
['securedRoutes' => $securedRoutesWithUserPermission]
77+
), 200, ['Content-Type' => 'application/javascript']);
78+
}
79+
}

README.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/SimLibaud/SilRouteSecurityBundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/SimLibaud/SilRouteSecurityBundle/?branch=master)
22
[![Build Status](https://scrutinizer-ci.com/g/SimLibaud/SilRouteSecurityBundle/badges/build.png?b=master)](https://scrutinizer-ci.com/g/SimLibaud/SilRouteSecurityBundle/build-status/master)
33
[![Code Coverage](https://scrutinizer-ci.com/g/SimLibaud/SilRouteSecurityBundle/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/SimLibaud/SilRouteSecurityBundle/?branch=master)
4-
[![SensioLabsInsight](https://insight.sensiolabs.com/projects/fbed9290-6c11-4461-b386-cf0cb46fc43e/mini.png)](https://insight.sensiolabs.com/projects/fbed9290-6c11-4461-b386-cf0cb46fc43e)
5-
4+
[![SymfonyInsight](https://insight.symfony.com/projects/0fe7d7d4-60ff-4f1c-a12f-c99d6510fafb/mini.svg)](https://insight.symfony.com/projects/0fe7d7d4-60ff-4f1c-a12f-c99d6510fafb)
65
# SilRouteSecurityBundle
76

87
This bundle provide a way to secure accesses to all routes of your application and adapt the view according to the logged user.
@@ -38,6 +37,16 @@ public function registerBundles()
3837
}
3938
```
4039

40+
## Add routes configuration
41+
42+
Routes are only required if you want to use the Javascript part of this bundle.
43+
44+
```yaml
45+
# app/config/routes/sil_route_security.yaml
46+
sil_route_security:
47+
resource: "@SilRouteSecurityBundle/Resources/config/routing.yaml"
48+
```
49+
4150
# Configuration
4251
4352
You can configure the bundle under the `sil_route_security` key.
@@ -120,7 +129,11 @@ For exemple, you can inject this service into your `UserFormType` to configure t
120129

121130
# Adapt template view
122131

123-
The bundle expose 3 twig functions that allow you to generate view according to the roles of user.
132+
The bundle expose :
133+
* Twig functions that allow you to generate view according to the roles of user.
134+
* Javascript object that allow you to generate view according to the roles of user.
135+
136+
## Twig
124137

125138
#### `hasUserAccessToRoute`
126139

@@ -168,6 +181,35 @@ The bundle expose 3 twig functions that allow you to generate view according to
168181
{% endif %}
169182
```
170183

184+
## Javascript
185+
186+
### Installation
187+
188+
To load it globally, add the following line to your template:
189+
190+
```html
191+
<script type="text/javascript" src="{{ asset('bundles/silroutesecurity/js/sil_route_security.min.js') }}"></script>
192+
<script src="{{ path('sil_route_security.export_js_secured_routes') }}"></script>
193+
```
194+
195+
### Usage
196+
197+
```javascript
198+
if (SilRouteSecurity.hasUserAccessToRoute('name_of_route')) {
199+
console.log('Current authenticated user has access to route')
200+
}
201+
202+
if (SilRouteSecurity.hasUserAccessToRoutes(['name_of_route_1', 'name_of_route_2'])) {
203+
console.log('Current authenticated user has access to all routes')
204+
}
205+
206+
if (SilRouteSecurity.hasUserAccessAtLeastOneRoute(['name_of_route_1', 'name_of_route_2'])) {
207+
console.log('Current authenticated user has access to one of this routes')
208+
}
209+
```
210+
211+
212+
171213
# Access denied behavior
172214

173215
When user access to secured route and does not have the right, an `AccessDeniedException` is throw. The framework will convert it to a 403 response.

Resources/config/routing.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
sil_route_security.export_js_secured_routes:
2+
path: /sil-route-security/export-js-secured-routes.js
3+
defaults: { _controller: sil_route_security.controller.export_js_secured_routes::exportAction }
4+
methods: [GET]

Resources/config/services.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,13 @@
4444
</service>
4545
<service id="Sil\RouteSecurityBundle\Role\RolesProvider" alias="sil_route_security.roles_provider"/>
4646

47+
<service id="sil_route_security.controller.export_js_secured_routes" class="Sil\RouteSecurityBundle\Controller\ExportJsSecuredRoutesController">
48+
<argument type="service" id="sil_route_security.access_control"></argument>
49+
<argument type="service" id="security.untracked_token_storage"></argument>
50+
<argument type="service" id="twig" />
51+
<argument>%kernel.cache_dir%</argument>
52+
<tag name="controller.service_arguments"></tag>
53+
</service>
54+
4755
</services>
4856
</container>

Resources/js/sil_route_security.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
const SilRouteSecurity = {
2+
allSecuredRoutesWithCurrentUserIsAccess: {},
3+
4+
/**
5+
* @param {String} route
6+
* @param {Boolean} hasAccess
7+
* @return {Object} SilRouteSecurity
8+
*/
9+
addSecuredRoutes: function(route, hasAccess) {
10+
this.allSecuredRoutesWithCurrentUserIsAccess[route] = hasAccess
11+
},
12+
13+
/**
14+
* @param {String} route
15+
* @return {Boolean}
16+
*/
17+
hasUserAccessToRoute: function(route) {
18+
return this.allSecuredRoutesWithCurrentUserIsAccess.hasOwnProperty(route)
19+
? this.allSecuredRoutesWithCurrentUserIsAccess[route]
20+
: true
21+
},
22+
23+
/**
24+
* @param {Array} route
25+
* @return {Boolean}
26+
*/
27+
hasUserAccessAtLeastOneRoute: function(routes) {
28+
for(let x = 0; x < routes.length; x++) {
29+
if (this.hasUserAccessToRoute(routes[x])) {
30+
return true
31+
}
32+
}
33+
34+
return false
35+
},
36+
37+
/**
38+
* @param {Array} route
39+
* @return {Boolean}
40+
*/
41+
hasUserAccessToRoutes: function(routes) {
42+
for(let x = 0; x < routes.length; x++) {
43+
if (!this.hasUserAccessToRoute(routes[x])) {
44+
return false
45+
}
46+
}
47+
48+
return true
49+
}
50+
}

Resources/public/js/sil_route_security.min.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// List of secured routes and associated permissions for current authenticated user
2+
{% for securedRoute, hasUserAccess in securedRoutes %}
3+
SilRouteSecurity.addSecuredRoutes('{{ securedRoute }}', {{ hasUserAccess ? 'true': 'false' }});
4+
{% endfor %}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Sil\RouteSecurityBundle\Tests\Controller;
5+
6+
7+
use Sil\RouteSecurityBundle\Tests\WebTestCase;
8+
use Symfony\Component\Security\Core\User\UserInterface;
9+
10+
class ExportJsSecuredRoutesControllerTest extends WebTestCase
11+
{
12+
public function testWithUnauthenticatedUser()
13+
{
14+
$client = static::createClient();
15+
16+
$crawler = $client->request('GET', '/sil-route-security/export-js-secured-routes.js');
17+
$response = $client->getResponse();
18+
19+
$this->assertEquals(500, $response->getStatusCode());
20+
}
21+
22+
public function testWithUserHasAccessToRoute()
23+
{
24+
$client = static::createClient();
25+
26+
$user = $this->createMock(UserInterface::class);
27+
$user->method('getRoles')->willReturn(['ROLE_APP_SECURED_ROUTE_TEST']);
28+
$client->loginUser($user);
29+
30+
$crawler = $client->request('GET', '/sil-route-security/export-js-secured-routes.js');
31+
$response = $client->getResponse();
32+
33+
$this->assertEquals(200, $response->getStatusCode());
34+
$this->assertEquals(<<<JAVASCRIPT
35+
// List of secured routes and associated permissions for current authenticated user
36+
SilRouteSecurity.addSecuredRoutes('app_secured_route_test', true);
37+
38+
JAVASCRIPT
39+
, $response->getContent());
40+
}
41+
42+
public function testWithUserHasNotAccessToRoute()
43+
{
44+
$client = static::createClient();
45+
46+
$user = $this->createMock(UserInterface::class);
47+
$user->method('getRoles')->willReturn([]);
48+
$client->loginUser($user);
49+
50+
$crawler = $client->request('GET', '/sil-route-security/export-js-secured-routes.js');
51+
$response = $client->getResponse();
52+
53+
$this->assertEquals(200, $response->getStatusCode());
54+
$this->assertEquals(<<<JAVASCRIPT
55+
// List of secured routes and associated permissions for current authenticated user
56+
SilRouteSecurity.addSecuredRoutes('app_secured_route_test', false);
57+
58+
JAVASCRIPT
59+
, $response->getContent());
60+
}
61+
}

Tests/Fixtures/AppKernel.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Sil\RouteSecurityBundle\Tests\Fixtures;
5+
6+
use Symfony\Component\Config\Loader\LoaderInterface;
7+
use Symfony\Component\HttpKernel\Kernel;
8+
9+
/**
10+
* App Test Kernel for functional tests.
11+
*/
12+
class AppKernel extends Kernel
13+
{
14+
public function __construct($environment, $debug)
15+
{
16+
parent::__construct($environment, $debug);
17+
}
18+
19+
public function registerBundles()
20+
{
21+
return array(
22+
new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
23+
new \Symfony\Bundle\TwigBundle\TwigBundle(),
24+
new \Sil\RouteSecurityBundle\SilRouteSecurityBundle(),
25+
new \Symfony\Bundle\SecurityBundle\SecurityBundle()
26+
);
27+
}
28+
29+
public function getRootDir()
30+
{
31+
return __DIR__;
32+
}
33+
34+
public function getProjectDir()
35+
{
36+
return __DIR__.'/../';
37+
}
38+
39+
public function getCacheDir()
40+
{
41+
return sys_get_temp_dir().'/'.Kernel::VERSION.'/sil-route-security/cache/'.$this->environment;
42+
}
43+
44+
public function getLogDir()
45+
{
46+
return sys_get_temp_dir().'/'.Kernel::VERSION.'/sil-route-security/logs';
47+
}
48+
49+
public function registerContainerConfiguration(LoaderInterface $loader)
50+
{
51+
$loader->load(__DIR__.'/config/base_config.yaml');
52+
}
53+
54+
public function serialize()
55+
{
56+
return serialize(array($this->getEnvironment(), $this->isDebug()));
57+
}
58+
59+
public function unserialize($str)
60+
{
61+
call_user_func_array(array($this, '__construct'), unserialize($str));
62+
}
63+
}

0 commit comments

Comments
 (0)