diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index 0fff7552e858d..fc798989dec78 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -18,6 +18,8 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\Security\Core\Role\RoleInterface; use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; +use Symfony\Component\Security\Core\Authorization\DebugAccessDecisionManager; /** * SecurityDataCollector. @@ -29,19 +31,22 @@ class SecurityDataCollector extends DataCollector private $tokenStorage; private $roleHierarchy; private $logoutUrlGenerator; + private $accessDecisionManager; /** * Constructor. * - * @param TokenStorageInterface|null $tokenStorage - * @param RoleHierarchyInterface|null $roleHierarchy - * @param LogoutUrlGenerator|null $logoutUrlGenerator + * @param TokenStorageInterface|null $tokenStorage + * @param RoleHierarchyInterface|null $roleHierarchy + * @param LogoutUrlGenerator|null $logoutUrlGenerator + * @param AccessDecisionManagerInterface|null $accessDecisionManager */ - public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, LogoutUrlGenerator $logoutUrlGenerator = null) + public function __construct(TokenStorageInterface $tokenStorage = null, RoleHierarchyInterface $roleHierarchy = null, LogoutUrlGenerator $logoutUrlGenerator = null, AccessDecisionManagerInterface $accessDecisionManager = null) { $this->tokenStorage = $tokenStorage; $this->roleHierarchy = $roleHierarchy; $this->logoutUrlGenerator = $logoutUrlGenerator; + $this->accessDecisionManager = $accessDecisionManager; } /** @@ -104,6 +109,20 @@ public function collect(Request $request, Response $response, \Exception $except 'supports_role_hierarchy' => null !== $this->roleHierarchy, ); } + + // collect voters and access decision manager information + if ($this->accessDecisionManager instanceof DebugAccessDecisionManager) { + $this->data['access_decision_log'] = $this->accessDecisionManager->getDecisionLog(); + $this->data['voter_strategy'] = $this->accessDecisionManager->getStrategy(); + + foreach ($this->accessDecisionManager->getVoters() as $voter) { + $this->data['voters'][] = get_class($voter); + } + } else { + $this->data['access_decision_log'] = array(); + $this->data['voter_strategy'] = 'unknown'; + $this->data['voters'] = array(); + } } /** @@ -187,6 +206,36 @@ public function getLogoutUrl() return $this->data['logout_url']; } + /** + * Returns the FQCN of the security voters enabled in the application. + * + * @return string[] + */ + public function getVoters() + { + return $this->data['voters']; + } + + /** + * Returns the strategy configured for the security voters. + * + * @return string + */ + public function getVoterStrategy() + { + return $this->data['voter_strategy']; + } + + /** + * Returns the log of the security decisions made by the access decision manager. + * + * @return array + */ + public function getAccessDecisionLog() + { + return $this->data['access_decision_log']; + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSecurityVotersPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSecurityVotersPass.php index 992d5a5f7c6f5..e2f8b3c645b61 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSecurityVotersPass.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSecurityVotersPass.php @@ -46,5 +46,9 @@ public function process(ContainerBuilder $container) } $container->getDefinition('security.access.decision_manager')->addMethodCall('setVoters', array(array_values($voters))); + + if ($container->hasDefinition('debug.security.access.decision_manager')) { + $container->getDefinition('debug.security.access.decision_manager')->addMethodCall('setVoters', array(array_values($voters))); + } } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 6043ed6292089..8427068727035 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -97,6 +97,13 @@ public function load(array $configs, ContainerBuilder $container) $this->aclLoad($config['acl'], $container); } + if ($container->hasParameter('kernel.debug') && $container->getParameter('kernel.debug')) { + $loader->load('security_debug.xml'); + + $definition = $container->findDefinition('security.authorization_checker'); + $definition->replaceArgument(2, new Reference('debug.security.access.decision_manager')); + } + // add some required classes for compilation $this->addClassesToCompile(array( 'Symfony\Component\Security\Http\Firewall', diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml index 329815fd608a1..738455358b0dd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml @@ -10,6 +10,7 @@ + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml new file mode 100644 index 0000000000000..4312d747417d1 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_debug.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig index dd724682d749e..c7c8147002df5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig @@ -119,4 +119,69 @@

The security component is disabled.

{% endif %} + + {% if collector.voters|default([]) is not empty %} +

Security Voters ({{ collector.voters|length }})

+ +
+
+ {{ collector.voterStrategy|default('unknown') }} + Strategy +
+
+ + + + + + + + + + + {% for voter in collector.voters %} + + + + + {% endfor %} + +
#Voter class
{{ loop.index }}{{ voter }}
+ {% endif %} + + {% if collector.accessDecisionLog|default([]) is not empty %} +

Access decision log

+ + + + + + + + + + + + + + + + + + {% for decision in collector.accessDecisionLog %} + + + + + + + {% endfor %} + +
#ResultAttributesObject
{{ loop.index }} + {{ decision.result + ? 'GRANTED' + : 'DENIED' + }} + {{ decision.attributes|length == 1 ? decision.attributes|first : profiler_dump(decision.attributes) }}{{ profiler_dump(decision.object) }}
+ {% endif %} {% endblock %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 6e07edeb3f2a0..67638ec532f96 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -235,6 +235,10 @@ table tbody ul { padding: 3px 7px; white-space: nowrap; } +.label.same-width { + min-width: 70px; + text-align: center; +} .label.status-success { background: {{ colors.success|raw }}; color: #FFF; } .label.status-warning { background: {{ colors.warning|raw }}; color: #FFF; } .label.status-error { background: {{ colors.error|raw }}; color: #FFF; } diff --git a/src/Symfony/Component/Security/Core/Authorization/DebugAccessDecisionManager.php b/src/Symfony/Component/Security/Core/Authorization/DebugAccessDecisionManager.php new file mode 100644 index 0000000000000..de7ec4d9a25db --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authorization/DebugAccessDecisionManager.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authorization; + +use Doctrine\Common\Util\ClassUtils; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * Decorates the original AccessDecisionManager class to log information + * about the security voters and the decisions made by them. + * + * @author Javier Eguiluz + * + * @internal + */ +class DebugAccessDecisionManager implements AccessDecisionManagerInterface +{ + private $manager; + private $strategy; + private $voters; + private $decisionLog = array(); + + public function __construct(AccessDecisionManager $manager) + { + $this->manager = $manager; + + // The strategy is stored in a private property of the decorated service + $reflection = new \ReflectionProperty($manager, 'strategy'); + $reflection->setAccessible(true); + $this->strategy = $reflection->getValue($manager); + } + + /** + * {@inheritdoc} + */ + public function decide(TokenInterface $token, array $attributes, $object = null) + { + $result = $this->manager->decide($token, $attributes, $object); + + $this->decisionLog[] = array( + 'attributes' => $attributes, + 'object' => $this->getStringRepresentation($object), + 'result' => $result, + ); + + return $result; + } + + /** + * {@inheritdoc} + */ + public function setVoters(array $voters) + { + $this->voters = $voters; + } + + /** + * @return string + */ + public function getStrategy() + { + // The $strategy property is misleading because it stores the name of its + // method (e.g. 'decideAffirmative') instead of the original strategy name + // (e.g. 'affirmative') + return strtolower(substr($this->strategy, 6)); + } + + /** + * @return array + */ + public function getVoters() + { + return $this->voters; + } + + /** + * @return array + */ + public function getDecisionLog() + { + return $this->decisionLog; + } + + /** + * @param mixed $object + * + * @return string + */ + private function getStringRepresentation($object) + { + if (null === $object) { + return 'NULL'; + } + + if (!is_object($object)) { + return sprintf('%s (%s)', gettype($object), $object); + } + + $objectClass = class_exists('Doctrine\Common\Util\ClassUtils') ? ClassUtils::getClass($object) : get_class($object); + + if (method_exists($object, 'getId')) { + $objectAsString = sprintf('ID: %s', $object->getId()); + } elseif (method_exists($object, '__toString')) { + $objectAsString = (string) $object; + } else { + $objectAsString = sprintf('object hash: %s', spl_object_hash($object)); + } + + return sprintf('%s (%s)', $objectClass, $objectAsString); + } +}