* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bundle\SecurityBundle\DependencyInjection; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\DefinitionDecorator; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Security\Core\Authorization\ExpressionLanguage; /** * SecurityExtension. * * @author Fabien Potencier * @author Johannes M. Schmitt */ class SecurityExtension extends Extension { private $requestMatchers = array(); private $expressions = array(); private $contextListeners = array(); private $listenerPositions = array('pre_auth', 'form', 'http', 'remember_me'); private $factories = array(); private $userProviderFactories = array(); private $expressionLanguage; private $statelessFirewallKeys = array(); public function __construct() { foreach ($this->listenerPositions as $position) { $this->factories[$position] = array(); } } public function load(array $configs, ContainerBuilder $container) { if (!array_filter($configs)) { return; } $mainConfig = $this->getConfiguration($configs, $container); $config = $this->processConfiguration($mainConfig, $configs); // load services $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('security.xml'); $loader->load('security_listeners.xml'); $loader->load('security_rememberme.xml'); $loader->load('templating_php.xml'); $loader->load('templating_twig.xml'); $loader->load('collectors.xml'); $loader->load('guard.xml'); if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { $container->removeDefinition('security.expression_language'); $container->removeDefinition('security.access.expression_voter'); } // set some global scalars $container->setParameter('security.access.denied_url', $config['access_denied_url']); $container->setParameter('security.authentication.manager.erase_credentials', $config['erase_credentials']); $container->setParameter('security.authentication.session_strategy.strategy', $config['session_fixation_strategy']); $container ->getDefinition('security.access.decision_manager') ->addArgument($config['access_decision_manager']['strategy']) ->addArgument($config['access_decision_manager']['allow_if_all_abstain']) ->addArgument($config['access_decision_manager']['allow_if_equal_granted_denied']) ; $container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']); $container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']); $this->createFirewalls($config, $container); $this->createAuthorization($config, $container); $this->createRoleHierarchy($config, $container); $container->getDefinition('security.authentication.guard_handler') ->replaceArgument(2, $this->statelessFirewallKeys); if ($config['encoders']) { $this->createEncoders($config['encoders'], $container); } // load ACL if (isset($config['acl'])) { $this->aclLoad($config['acl'], $container); } // add some required classes for compilation $this->addClassesToCompile(array( 'Symfony\Component\Security\Http\Firewall', 'Symfony\Component\Security\Core\User\UserProviderInterface', 'Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager', 'Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage', 'Symfony\Component\Security\Core\Authorization\AccessDecisionManager', 'Symfony\Component\Security\Core\Authorization\AuthorizationChecker', 'Symfony\Component\Security\Core\Authorization\Voter\VoterInterface', 'Symfony\Bundle\SecurityBundle\Security\FirewallMap', 'Symfony\Bundle\SecurityBundle\Security\FirewallContext', 'Symfony\Component\HttpFoundation\RequestMatcher', )); } private function aclLoad($config, ContainerBuilder $container) { $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('security_acl.xml'); if (isset($config['cache']['id'])) { $container->setAlias('security.acl.cache', $config['cache']['id']); } $container->getDefinition('security.acl.voter.basic_permissions')->addArgument($config['voter']['allow_if_object_identity_unavailable']); // custom ACL provider if (isset($config['provider'])) { $container->setAlias('security.acl.provider', $config['provider']); return; } $this->configureDbalAclProvider($config, $container, $loader); } private function configureDbalAclProvider(array $config, ContainerBuilder $container, $loader) { $loader->load('security_acl_dbal.xml'); if (null !== $config['connection']) { $container->setAlias('security.acl.dbal.connection', sprintf('doctrine.dbal.%s_connection', $config['connection'])); } $container ->getDefinition('security.acl.dbal.schema_listener') ->addTag('doctrine.event_listener', array( 'connection' => $config['connection'], 'event' => 'postGenerateSchema', 'lazy' => true, )) ; $container->getDefinition('security.acl.cache.doctrine')->addArgument($config['cache']['prefix']); $container->setParameter('security.acl.dbal.class_table_name', $config['tables']['class']); $container->setParameter('security.acl.dbal.entry_table_name', $config['tables']['entry']); $container->setParameter('security.acl.dbal.oid_table_name', $config['tables']['object_identity']); $container->setParameter('security.acl.dbal.oid_ancestors_table_name', $config['tables']['object_identity_ancestors']); $container->setParameter('security.acl.dbal.sid_table_name', $config['tables']['security_identity']); } private function createRoleHierarchy(array $config, ContainerBuilder $container) { if (!isset($config['role_hierarchy']) || 0 === \count($config['role_hierarchy'])) { $container->removeDefinition('security.access.role_hierarchy_voter'); return; } $container->setParameter('security.role_hierarchy.roles', $config['role_hierarchy']); $container->removeDefinition('security.access.simple_role_voter'); } private function createAuthorization($config, ContainerBuilder $container) { if (!$config['access_control']) { return; } $this->addClassesToCompile(array( 'Symfony\\Component\\Security\\Http\\AccessMap', )); foreach ($config['access_control'] as $access) { $matcher = $this->createRequestMatcher( $container, $access['path'], $access['host'], $access['methods'], $access['ips'] ); $attributes = $access['roles']; if ($access['allow_if']) { $attributes[] = $this->createExpression($container, $access['allow_if']); } $container->getDefinition('security.access_map') ->addMethodCall('add', array($matcher, $attributes, $access['requires_channel'])); } } private function createFirewalls($config, ContainerBuilder $container) { if (!isset($config['firewalls'])) { return; } $firewalls = $config['firewalls']; $providerIds = $this->createUserProviders($config, $container); // make the ContextListener aware of the configured user providers $definition = $container->getDefinition('security.context_listener'); $arguments = $definition->getArguments(); $userProviders = array(); foreach ($providerIds as $userProviderId) { $userProviders[] = new Reference($userProviderId); } $arguments[1] = $userProviders; $definition->setArguments($arguments); // load firewall map $mapDef = $container->getDefinition('security.firewall.map'); $map = $authenticationProviders = array(); foreach ($firewalls as $name => $firewall) { list($matcher, $listeners, $exceptionListener, $logoutListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds); $contextId = 'security.firewall.map.context.'.$name; $context = $container->setDefinition($contextId, new DefinitionDecorator('security.firewall.context')); $context ->replaceArgument(0, $listeners) ->replaceArgument(1, $exceptionListener) ->replaceArgument(2, $logoutListener) ; $map[$contextId] = $matcher; } $mapDef->replaceArgument(1, $map); // add authentication providers to authentication manager $authenticationProviders = array_map(function ($id) { return new Reference($id); }, array_values(array_unique($authenticationProviders))); $container ->getDefinition('security.authentication.manager') ->replaceArgument(0, $authenticationProviders) ; } private function createFirewall(ContainerBuilder $container, $id, $firewall, &$authenticationProviders, $providerIds) { // Matcher $matcher = null; if (isset($firewall['request_matcher'])) { $matcher = new Reference($firewall['request_matcher']); } elseif (isset($firewall['pattern']) || isset($firewall['host'])) { $pattern = isset($firewall['pattern']) ? $firewall['pattern'] : null; $host = isset($firewall['host']) ? $firewall['host'] : null; $methods = isset($firewall['methods']) ? $firewall['methods'] : array(); $matcher = $this->createRequestMatcher($container, $pattern, $host, $methods); } // Security disabled? if (false === $firewall['security']) { return array($matcher, array(), null, null); } // Provider id (take the first registered provider if none defined) if (isset($firewall['provider'])) { $defaultProvider = $this->getUserProviderId($firewall['provider']); } else { $defaultProvider = reset($providerIds); } // Register listeners $listeners = array(); // Channel listener $listeners[] = new Reference('security.channel_listener'); // Context serializer listener if (false === $firewall['stateless']) { $contextKey = $id; if (isset($firewall['context'])) { $contextKey = $firewall['context']; } $listeners[] = new Reference($this->createContextListener($container, $contextKey)); $sessionStrategyId = 'security.authentication.session_strategy'; } else { $this->statelessFirewallKeys[] = $id; $sessionStrategyId = 'security.authentication.session_strategy_noop'; } $container->setAlias(new Alias('security.authentication.session_strategy.'.$id, false), $sessionStrategyId); // Logout listener $logoutListenerId = null; if (isset($firewall['logout'])) { $logoutListenerId = 'security.logout_listener.'.$id; $logoutListener = $container->setDefinition($logoutListenerId, new DefinitionDecorator('security.logout_listener')); $logoutListener->replaceArgument(3, array( 'csrf_parameter' => $firewall['logout']['csrf_parameter'], 'csrf_token_id' => $firewall['logout']['csrf_token_id'], 'logout_path' => $firewall['logout']['path'], )); // add logout success handler if (isset($firewall['logout']['success_handler'])) { $logoutSuccessHandlerId = $firewall['logout']['success_handler']; } else { $logoutSuccessHandlerId = 'security.logout.success_handler.'.$id; $logoutSuccessHandler = $container->setDefinition($logoutSuccessHandlerId, new DefinitionDecorator('security.logout.success_handler')); $logoutSuccessHandler->replaceArgument(1, $firewall['logout']['target']); } $logoutListener->replaceArgument(2, new Reference($logoutSuccessHandlerId)); // add CSRF provider if (isset($firewall['logout']['csrf_token_generator'])) { $logoutListener->addArgument(new Reference($firewall['logout']['csrf_token_generator'])); } // add session logout handler if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) { $logoutListener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); } // add cookie logout handler if (\count($firewall['logout']['delete_cookies']) > 0) { $cookieHandlerId = 'security.logout.handler.cookie_clearing.'.$id; $cookieHandler = $container->setDefinition($cookieHandlerId, new DefinitionDecorator('security.logout.handler.cookie_clearing')); $cookieHandler->addArgument($firewall['logout']['delete_cookies']); $logoutListener->addMethodCall('addHandler', array(new Reference($cookieHandlerId))); } // add custom handlers foreach ($firewall['logout']['handlers'] as $handlerId) { $logoutListener->addMethodCall('addHandler', array(new Reference($handlerId))); } // register with LogoutUrlGenerator $container ->getDefinition('security.logout_url_generator') ->addMethodCall('registerListener', array( $id, $firewall['logout']['path'], $firewall['logout']['csrf_token_id'], $firewall['logout']['csrf_parameter'], isset($firewall['logout']['csrf_token_generator']) ? new Reference($firewall['logout']['csrf_token_generator']) : null, )) ; } // Determine default entry point $configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null; // Authentication listeners list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $configuredEntryPoint); $listeners = array_merge($listeners, $authListeners); // Switch user listener if (isset($firewall['switch_user'])) { $listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider)); } // Access listener $listeners[] = new Reference('security.access_listener'); // Exception listener $exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $configuredEntryPoint ?: $defaultEntryPoint, $firewall['stateless'])); $container->setAlias(new Alias('security.user_checker.'.$id, false), $firewall['user_checker']); return array($matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null); } private function createContextListener($container, $contextKey) { if (isset($this->contextListeners[$contextKey])) { return $this->contextListeners[$contextKey]; } $listenerId = 'security.context_listener.'.\count($this->contextListeners); $listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.context_listener')); $listener->replaceArgument(2, $contextKey); return $this->contextListeners[$contextKey] = $listenerId; } private function createAuthenticationListeners($container, $id, $firewall, &$authenticationProviders, $defaultProvider, $defaultEntryPoint) { $listeners = array(); $hasListeners = false; foreach ($this->listenerPositions as $position) { foreach ($this->factories[$position] as $factory) { $key = str_replace('-', '_', $factory->getKey()); if (isset($firewall[$key])) { $userProvider = isset($firewall[$key]['provider']) ? $this->getUserProviderId($firewall[$key]['provider']) : $defaultProvider; list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); $listeners[] = new Reference($listenerId); $authenticationProviders[] = $provider; $hasListeners = true; } } } // Anonymous if (isset($firewall['anonymous'])) { $listenerId = 'security.authentication.listener.anonymous.'.$id; $container ->setDefinition($listenerId, new DefinitionDecorator('security.authentication.listener.anonymous')) ->replaceArgument(1, $firewall['anonymous']['secret']) ; $listeners[] = new Reference($listenerId); $providerId = 'security.authentication.provider.anonymous.'.$id; $container ->setDefinition($providerId, new DefinitionDecorator('security.authentication.provider.anonymous')) ->replaceArgument(0, $firewall['anonymous']['secret']) ; $authenticationProviders[] = $providerId; $hasListeners = true; } if (false === $hasListeners) { throw new InvalidConfigurationException(sprintf('No authentication listener registered for firewall "%s".', $id)); } return array($listeners, $defaultEntryPoint); } private function createEncoders($encoders, ContainerBuilder $container) { $encoderMap = array(); foreach ($encoders as $class => $encoder) { $encoderMap[$class] = $this->createEncoder($encoder, $container); } $container ->getDefinition('security.encoder_factory.generic') ->setArguments(array($encoderMap)) ; } private function createEncoder($config, ContainerBuilder $container) { // a custom encoder service if (isset($config['id'])) { return new Reference($config['id']); } // plaintext encoder if ('plaintext' === $config['algorithm']) { $arguments = array($config['ignore_case']); return array( 'class' => new Parameter('security.encoder.plain.class'), 'arguments' => $arguments, ); } // pbkdf2 encoder if ('pbkdf2' === $config['algorithm']) { return array( 'class' => new Parameter('security.encoder.pbkdf2.class'), 'arguments' => array( $config['hash_algorithm'], $config['encode_as_base64'], $config['iterations'], $config['key_length'], ), ); } // bcrypt encoder if ('bcrypt' === $config['algorithm']) { return array( 'class' => new Parameter('security.encoder.bcrypt.class'), 'arguments' => array($config['cost']), ); } // message digest encoder return array( 'class' => new Parameter('security.encoder.digest.class'), 'arguments' => array( $config['algorithm'], $config['encode_as_base64'], $config['iterations'], ), ); } // Parses user providers and returns an array of their ids private function createUserProviders($config, ContainerBuilder $container) { $providerIds = array(); foreach ($config['providers'] as $name => $provider) { $id = $this->createUserDaoProvider($name, $provider, $container); $providerIds[] = $id; } return $providerIds; } // Parses a tag and returns the id for the related user provider service private function createUserDaoProvider($name, $provider, ContainerBuilder $container) { $name = $this->getUserProviderId($name); // Doctrine Entity and In-memory DAO provider are managed by factories foreach ($this->userProviderFactories as $factory) { $key = str_replace('-', '_', $factory->getKey()); if (!empty($provider[$key])) { $factory->create($container, $name, $provider[$key]); return $name; } } // Existing DAO service provider if (isset($provider['id'])) { $container->setAlias($name, new Alias($provider['id'], false)); return $provider['id']; } // Chain provider if (isset($provider['chain'])) { $providers = array(); foreach ($provider['chain']['providers'] as $providerName) { $providers[] = new Reference($this->getUserProviderId($providerName)); } $container ->setDefinition($name, new DefinitionDecorator('security.user.provider.chain')) ->addArgument($providers); return $name; } throw new InvalidConfigurationException(sprintf('Unable to create definition for "%s" user provider', $name)); } private function getUserProviderId($name) { return 'security.user.provider.concrete.'.strtolower($name); } private function createExceptionListener($container, $config, $id, $defaultEntryPoint, $stateless) { $exceptionListenerId = 'security.exception_listener.'.$id; $listener = $container->setDefinition($exceptionListenerId, new DefinitionDecorator('security.exception_listener')); $listener->replaceArgument(3, $id); $listener->replaceArgument(4, null === $defaultEntryPoint ? null : new Reference($defaultEntryPoint)); $listener->replaceArgument(8, $stateless); // access denied handler setup if (isset($config['access_denied_handler'])) { $listener->replaceArgument(6, new Reference($config['access_denied_handler'])); } elseif (isset($config['access_denied_url'])) { $listener->replaceArgument(5, $config['access_denied_url']); } return $exceptionListenerId; } private function createSwitchUserListener($container, $id, $config, $defaultProvider) { $userProvider = isset($config['provider']) ? $this->getUserProviderId($config['provider']) : $defaultProvider; $switchUserListenerId = 'security.authentication.switchuser_listener.'.$id; $listener = $container->setDefinition($switchUserListenerId, new DefinitionDecorator('security.authentication.switchuser_listener')); $listener->replaceArgument(1, new Reference($userProvider)); $listener->replaceArgument(2, new Reference('security.user_checker.'.$id)); $listener->replaceArgument(3, $id); $listener->replaceArgument(6, $config['parameter']); $listener->replaceArgument(7, $config['role']); return $switchUserListenerId; } private function createExpression($container, $expression) { if (isset($this->expressions[$id = 'security.expression.'.sha1($expression)])) { return $this->expressions[$id]; } $container ->register($id, 'Symfony\Component\ExpressionLanguage\SerializedParsedExpression') ->setPublic(false) ->addArgument($expression) ->addArgument(serialize($this->getExpressionLanguage()->parse($expression, array('token', 'user', 'object', 'roles', 'request', 'trust_resolver'))->getNodes())) ; return $this->expressions[$id] = new Reference($id); } private function createRequestMatcher($container, $path = null, $host = null, $methods = array(), $ip = null, array $attributes = array()) { if ($methods) { $methods = array_map('strtoupper', (array) $methods); } $serialized = serialize(array($path, $host, $methods, $ip, $attributes)); $id = 'security.request_matcher.'.md5($serialized).sha1($serialized); if (isset($this->requestMatchers[$id])) { return $this->requestMatchers[$id]; } // only add arguments that are necessary $arguments = array($path, $host, $methods, $ip, $attributes); while (\count($arguments) > 0 && !end($arguments)) { array_pop($arguments); } $container ->register($id, '%security.matcher.class%') ->setPublic(false) ->setArguments($arguments) ; return $this->requestMatchers[$id] = new Reference($id); } public function addSecurityListenerFactory(SecurityFactoryInterface $factory) { $this->factories[$factory->getPosition()][] = $factory; } public function addUserProviderFactory(UserProviderFactoryInterface $factory) { $this->userProviderFactories[] = $factory; } /** * Returns the base path for the XSD files. * * @return string The XSD base path */ public function getXsdValidationBasePath() { return __DIR__.'/../Resources/config/schema'; } public function getNamespace() { return 'http://symfony.com/schema/dic/security'; } public function getConfiguration(array $config, ContainerBuilder $container) { // first assemble the factories return new MainConfiguration($this->factories, $this->userProviderFactories); } private function getExpressionLanguage() { if (null === $this->expressionLanguage) { if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); } $this->expressionLanguage = new ExpressionLanguage(); } return $this->expressionLanguage; } }