8000 feature #17887 Show more information in the security profiler (javier… · symfony/symfony@5ebecca · GitHub
[go: up one dir, main page]

Skip to content

Commit 5ebecca

Browse files
committed
feature #17887 Show more information in the security profiler (javiereguiluz)
This PR was squashed before being merged into the 3.1-dev branch (closes #17887). Discussion ---------- Show more information in the security profiler | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #17856 | License | MIT | Doc PR | - This is an early prototype to explore the feature of displaying more information in the security panel. Example: ![profiler_security](https://cloud.githubusercontent.com/assets/73419/13221929/0235fc46-d97e-11e5-981a-249b7148f3a6.png) Commits ------- b12152d Show more information in the security profiler
2 parents 0e5ac97 + b12152d commit 5ebecca

File tree

8 files changed

+266
-4
lines changed

8 files changed

+266
-4
lines changed

src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
1919
use Symfony\Component\Security\Core\Role\RoleInterface;
2020
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
21+
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
22+
use Symfony\Component\Security\Core\Authorization\DebugAccessDecisionManager;
2123

2224
/**
2325
* SecurityDataCollector.
@@ -29,19 +31,22 @@ class SecurityDataCollector extends DataCollector
2931
private $tokenStorage;
3032
private $roleHierarchy;
3133
private $logoutUrlGenerator;
34+
private $accessDecisionManager;
3235

3336
/**
3437
* Constructor.
3538
*
36-
* @param TokenStorageInterface|null $tokenStorage
37-
* @param RoleHierarchyInterface|null $roleHierarchy
38-
* @param LogoutUrlGenerator|null $logoutUrlGenerator
39+
* @param TokenStorageInterface|null $tokenStorage
40+
67E6 * @param RoleHierarchyInterface|null $roleHierarchy
41+
* @param LogoutUrlGenerator|null $logoutUrlGenerator
42+
* @param AccessDecisionManagerInterface|null $accessDecisionManager
3943
*/
40-
public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, LogoutUrlGenerator $logoutUrlGenerator = null)
44+
public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, LogoutUrlGenerator $logoutUrlGenerator = null, AccessDecisionManagerInterface $accessDecisionManager = null)
4145
{
4246
$this->tokenStorage = $tokenStorage;
4347
$this->roleHierarchy = $roleHierarchy;
4448
$this->logoutUrlGenerator = $logoutUrlGenerator;
49+
$this->accessDecisionManager = $accessDecisionManager;
4550
}
4651

4752
/**
@@ -104,6 +109,20 @@ public function collect(Request $request, Response $response, \Exception $except
104109
'supports_role_hierarchy' => null !== $this->roleHierarchy,
105110
);
106111
}
112+
113+
// collect voters and access decision manager information
114+
if ($this->accessDecisionManager instanceof DebugAccessDecisionManager) {
115+
$this->data['access_decision_log'] = $this->accessDecisionManager->getDecisionLog();
116+
$this->data['voter_strategy'] = $this->accessDecisionManager->getStrategy();
117+
118+
foreach ($this->accessDecisionManager->getVoters() as $voter) {
119+
$this->data['voters'][] = get_class($voter);
120+
}
121+
} else {
122+
$this->data['access_decision_log'] = array();
123+
$this->data['voter_strategy'] = 'unknown';
124+
$this->data['voters'] = array();
125+
}
107126
}
108127

109128
/**
@@ -187,6 +206,36 @@ public function getLogoutUrl()
187206
return $this->data['logout_url'];
188207
}
189208

209+
/**
210+
* Returns the FQCN of the security voters enabled in the application.
211+
*
212+
* @return string[]
213+
*/
214+
public function getVoters()
215+
{
216+
return $this->data['voters'];
217+
}
218+
219+
/**
220+
* Returns the strategy configured for the security voters.
221+
*
222+
* @return string
223+
*/
224+
public function getVoterStrategy()
225+
{
226+
return $this->data['voter_strategy'];
227+
}
228+
229+
/**
230+
* Returns the log of the security decisions made by the access decision manager.
231+
*
232+
* @return array
233+
*/
234+
public function getAccessDecisionLog()
235+
{
236+
return $this->data['access_decision_log'];
237+
}
238+
190239
/**
191240
* {@inheritdoc}
192241
*/

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSecurityVotersPass.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,9 @@ public function process(ContainerBuilder $container)
4646
}
4747

4848
$container->getDefinition('security.access.decision_manager')->addMethodCall('setVoters', array(array_values($voters)));
49+
50+
if ($container->hasDefinition('debug.security.access.decision_manager')) {
51+
$container->getDefinition('debug.security.access.decision_manager')->addMethodCall('setVoters', array(array_values($voters)));
52+
}
4953
}
5054
}

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ public function load(array $configs, ContainerBuilder $container)
9797
$this->aclLoad($config['acl'], $container);
9898
}
9999

100+
if ($container->hasParameter('kernel.debug') && $container->getParameter('kernel.debug')) {
101+
$loader->load('security_debug.xml');
102+
103+
$definition = $container->findDefinition('security.authorization_checker');
104+
$definition->replaceArgument(2, new Reference('debug.security.access.decision_manager'));
105+
}
106+
100107
// add some required classes for compilation
101108
$this->addClassesToCompile(array(
102109
'Symfony\Component\Security\Http\Firewall',

src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<argument type="service" id="security.token_storage" on-invalid="ignore" />
1111
<argument type="service" id="security.role_hierarchy" />
1212
<argument type="service" id="security.logout_url_generator" />
13+
<argument type="service" id="debug.security.access.decision_manager" />
1314
</service>
1415
</services>
1516
</container>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
<service id="debug.security.access.decision_manager" class="Symfony\Component\Security\Core\Authorization\DebugAccessDecisionManager" decorates="security.access.decision_manager" public="false">
9+
<argument type="service" id="debug.security.access.decision_manager.inner" />
10+
</service>
11+
</services>
12+
</container>

src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,69 @@
119119
<p>The security component is disabled.</p>
120120
</div>
121121
{% endif %}
122+
123+
{% if collector.voters|default([]) is not empty %}
124+
<h2>Security Voters <small>({{ collector.voters|length }})</small></h2>
125+
126+
<div class="metrics">
127+
<div class="metric">
128+
<span class="value">{{ collector.voterStrategy|default('unknown') }}</span>
129+
<span class="label">Strategy</span>
130+
</div>
131+
</div>
132+
133+
<table class="voters">
134+
<thead>
135+
<tr>
136+
<th>#</th>
137+
<th>Voter class</th>
138+
</tr>
139+
</thead>
140+
141+
<tbody>
142+
{% for voter in collector.voters %}
143+
<tr>
144+
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
145+
<td class="font-normal">{{ voter }}</td>
146+
</tr>
147+
{% endfor %}
148+
</tbody>
149+
</table>
150+
{% endif %}
151+
152+
{% if collector.accessDecisionLog|default([]) is not empty %}
153+
<h2>Access decision log</h2>
154+
155+
<table class="decision-log">
156+
<col style="width: 30px">
157+
<col style="width: 120px">
158+
<col style="width: 25%">
159+
<col style="width: 60%">
160+
161+
<thead>
162+
<tr>
163+
<th>#</th>
164+
<th>Result</th>
165+
<th>Attributes</th>
166+
<th>Object</th>
167+
</tr>
168+
</thead>
169+
170+
<tbody>
171+
{% for decision in collector.accessDecisionLog %}
172+
<tr>
173+
<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
174+
<td class="font-normal">
175+
{{ decision.result
176+
? '<span class="label status-success same-width">GRANTED</span>'
177+
: '<span class="label status-error same-width">DENIED</span>'
178+
}}
179+
</td>
180+
<td>{{ decision.attributes|length == 1 ? decision.attributes|first : profiler_dump(decision.attributes) }}</td>
181+
<td>{{ profiler_dump(decision.object) }}</td>
182+
</tr>
183+
{% endfor %}
184+
</tbody>
185+
</table>
186+
{% endif %}
122187
{% endblock %}

src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ table tbody ul {
235235
padding: 3px 7px;
236236
white-space: nowrap;
237237
}
238+
.label.same-width {
239+
min-width: 70px;
240+
text-align: center;
241+
}
238242
.label.status-success { background: {{ colors.success|raw }}; color: #FFF; }
239243
.label.status-warning { background: {{ colors.warning|raw }}; color: #FFF; }
240244
.label.status-error { background: {{ colors.error|raw }}; color: #FFF; }
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Authorization;
13+
14+
use Doctrine\Common\Util\ClassUtils;
15+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
16+
17+
/**
18+
* Decorates the original AccessDecisionManager class to log information
19+
* about the security voters and the decisions made by them.
20+
*
21+
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
22+
*
23+
* @internal
24+
*/
25+
class DebugAccessDecisionManager implements AccessDecisionManagerInterface
26+
{
27+
private $manager;
28+
private $strategy;
29+
private $voters;
30+
private $decisionLog = array();
31+
32+
public function __construct(AccessDecisionManager $manager)
33+
{
34+
$this->manager = $manager;
35+
36+
// The strategy is stored in a private property of the decorated service
37+
$reflection = new \ReflectionProperty($manager, 'strategy');
38+
$reflection->setAccessible(true);
39+
$this->strategy = $reflection->getValue($manager);
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function decide(TokenInterface $token, array $attributes, $object = null)
46+
{
47+
$result = $this->manager->decide($token, $attributes, $object);
48+
49+
$this->decisionLog[] = array(
50+
'attributes' => $attributes,
51+
'object' => $this->getStringRepresentation($object),
52+
'result' => $result,
53+
);
54+
55+
return $result;
56+
}
57+
58+
/**
59+
* {@inheritdoc}
60+
*/
61+
public function setVoters(array $voters)
62+
{
63+
$this->voters = $voters;
64+
}
65+
66+
/**
67+
* @return string
68+
*/
69+
public function getStrategy()
70+
{
71+
// The $strategy property is misleading because it stores the name of its
72+
// method (e.g. 'decideAffirmative') instead of the original strategy name
73+
// (e.g. 'affirmative')
74+
return strtolower(substr($this->strategy, 6));
75+
}
76+
77+
/**
78+
* @return array
79+
*/
80+
public function getVoters()
81+
{
82+
return $this->voters;
83+
}
84+
85+
/**
86+
* @return array
87+
*/
88+
public function getDecisionLog()
89+
{
90+
return $this->decisionLog;
91+
}
92+
93+
/**
94+
* @param mixed $object
95+
*
96+
* @return string
97+
*/
98+
private function getStringRepresentation($object)
99+
{
100+
if (null === $object) {
101+
return 'NULL';
102+
}
103+
104+
if (!is_object($object)) {
105+
return sprintf('%s (%s)', gettype($object), $object);
106+
}
107+
108+
$objectClass = class_exists('Doctrine\Common\Util\ClassUtils') ? ClassUtils::getClass($object) : get_class($object);
109+
110+
if (method_exists($object, 'getId')) {
111+
$objectAsString = sprintf('ID: %s', $object->getId());
112+
} elseif (method_exists($object, '__toString')) {
113+
$objectAsString = (string) $object;
114+
} else {
115+
$objectAsString = sprintf('object hash: %s', spl_object_hash($object));
116+
}
117+
118+
return sprintf('%s (%s)', $objectClass, $objectAsString);
119+
}
120+
}

0 commit comments

Comments
 (0)
0