SecurityExtension.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Bundle\SecurityBundle\DependencyInjection;
  11. use Symfony\Component\DependencyInjection\Configuration\Processor;
  12. use Symfony\Component\DependencyInjection\Configuration\Builder\TreeBuilder;
  13. use Symfony\Component\DependencyInjection\DefinitionDecorator;
  14. use Symfony\Component\DependencyInjection\Alias;
  15. use Symfony\Component\HttpKernel\DependencyInjection\Extension;
  16. use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
  17. use Symfony\Component\Config\Resource\FileResource;
  18. use Symfony\Component\DependencyInjection\ContainerBuilder;
  19. use Symfony\Component\DependencyInjection\Reference;
  20. use Symfony\Component\DependencyInjection\Parameter;
  21. use Symfony\Component\DependencyInjection\Definition;
  22. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
  23. use Symfony\Component\Config\FileLocator;
  24. use Symfony\Component\HttpFoundation\RequestMatcher;
  25. /**
  26. * SecurityExtension.
  27. *
  28. * @author Fabien Potencier <fabien.potencier@symfony-project.com>
  29. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  30. */
  31. class SecurityExtension extends Extension
  32. {
  33. protected $requestMatchers = array();
  34. protected $contextListeners = array();
  35. protected $listenerPositions = array('pre_auth', 'form', 'http', 'remember_me');
  36. protected $configuration;
  37. protected $factories;
  38. public function __construct()
  39. {
  40. $this->configuration = new Configuration();
  41. }
  42. public function configLoad(array $configs, ContainerBuilder $container)
  43. {
  44. $processor = new Processor();
  45. // first assemble the factories
  46. $factories = $this->createListenerFactories($container, $processor->process($this->configuration->getFactoryConfigTree(), $configs));
  47. // normalize and merge the actual configuration
  48. $tree = $this->configuration->getMainConfigTree($factories);
  49. $config = $processor->process($tree, $configs);
  50. // load services
  51. $loader = new XmlFileLoader($container, new FileLocator(array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config')));
  52. $loader->load('security.xml');
  53. $loader->load('security_listeners.xml');
  54. $loader->load('security_rememberme.xml');
  55. $loader->load('templating_php.xml');
  56. $loader->load('templating_twig.xml');
  57. $loader->load('collectors.xml');
  58. // set some global scalars
  59. $container->setParameter('security.access.denied_url', $config['access_denied_url']);
  60. $container->setParameter('security.authentication.session_strategy.strategy', $config['session_fixation_strategy']);
  61. $this->createFirewalls($config, $container);
  62. $this->createAuthorization($config, $container);
  63. $this->createRoleHierarchy($config, $container);
  64. }
  65. public function aclLoad(array $configs, ContainerBuilder $container)
  66. {
  67. $processor = new Processor();
  68. $config = $processor->process($this->configuration->getAclConfigTree(), $configs);
  69. $loader = new XmlFileLoader($container, new FileLocator(array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config')));
  70. $loader->load('security_acl.xml');
  71. if (isset($config['connection'])) {
  72. $container->setAlias('security.acl.dbal.connection', sprintf('doctrine.dbal.%s_connection', $config['connection']));
  73. }
  74. if (isset($config['cache'])) {
  75. $container->setAlias('security.acl.cache', sprintf('security.acl.cache.%s', $config['cache']));
  76. }
  77. }
  78. /**
  79. * Returns the base path for the XSD files.
  80. *
  81. * @return string The XSD base path
  82. */
  83. public function getXsdValidationBasePath()
  84. {
  85. return __DIR__.'/../Resources/config/schema';
  86. }
  87. public function getNamespace()
  88. {
  89. return 'http://www.symfony-project.org/schema/dic/security';
  90. }
  91. public function getAlias()
  92. {
  93. return 'security';
  94. }
  95. /**
  96. * Loads the web configuration.
  97. *
  98. * @param array $config An array of configuration settings
  99. * @param ContainerBuilder $container A ContainerBuilder instance
  100. */
  101. protected function createRoleHierarchy($config, ContainerBuilder $container)
  102. {
  103. if (!isset($config['role_hierarchy'])) {
  104. $container->remove('security.access.role_hierarchy_voter');
  105. return;
  106. }
  107. $container->setParameter('security.role_hierarchy.roles', $config['role_hierarchy']);
  108. $container->remove('security.access.simple_role_voter');
  109. }
  110. protected function createAuthorization($config, ContainerBuilder $container)
  111. {
  112. foreach ($config['access_control'] as $access) {
  113. $matcher = $this->createRequestMatcher(
  114. $container,
  115. $access['path'],
  116. $access['host'],
  117. count($access['methods']) === 0 ? null : $access['methods'],
  118. $access['ip'],
  119. $access['attributes']
  120. );
  121. $container->getDefinition('security.access_map')
  122. ->addMethodCall('add', array($matcher, $access['roles'], $access['requires_channel']));
  123. }
  124. }
  125. protected function createFirewalls($config, ContainerBuilder $container)
  126. {
  127. if (!isset($config['firewalls'])) {
  128. return;
  129. }
  130. $firewalls = $config['firewalls'];
  131. $providerIds = $this->createUserProviders($config, $container);
  132. $this->createEncoders($config, $container);
  133. // make the ContextListener aware of the configured user providers
  134. $definition = $container->getDefinition('security.context_listener');
  135. $arguments = $definition->getArguments();
  136. $userProviders = array();
  137. foreach ($providerIds as $userProviderId) {
  138. $userProviders[] = new Reference($userProviderId);
  139. }
  140. $arguments[1] = $userProviders;
  141. $definition->setArguments($arguments);
  142. // create security listener factories
  143. $factories = $this->createListenerFactories($container, $config);
  144. // load firewall map
  145. $mapDef = $container->getDefinition('security.firewall.map');
  146. $map = array();
  147. foreach ($firewalls as $name => $firewall) {
  148. list($matcher, $listeners, $exceptionListener) = $this->createFirewall($container, $name, $firewall, $providerIds, $factories);
  149. $contextId = 'security.firewall.map.context.'.$name;
  150. $context = $container->setDefinition($contextId, new DefinitionDecorator('security.firewall.context'));
  151. $context
  152. ->setArgument(0, $listeners)
  153. ->setArgument(1, $exceptionListener)
  154. ;
  155. $map[$contextId] = $matcher;
  156. }
  157. $mapDef->setArgument(1, $map);
  158. }
  159. protected function createFirewall(ContainerBuilder $container, $id, $firewall, $providerIds, array $factories)
  160. {
  161. // Matcher
  162. $i = 0;
  163. $matcher = null;
  164. if (isset($firewall['request_matcher'])) {
  165. $matcher = new Reference($firewall['request_matcher']);
  166. } else if (isset($firewall['pattern'])) {
  167. $matcher = $this->createRequestMatcher($container, $firewall['pattern']);
  168. }
  169. // Security disabled?
  170. if (false === $firewall['security']) {
  171. return array($matcher, array(), null);
  172. }
  173. // Provider id (take the first registered provider if none defined)
  174. if (isset($firewall['provider'])) {
  175. $defaultProvider = $this->getUserProviderId($firewall['provider']);
  176. } else {
  177. $defaultProvider = reset($providerIds);
  178. }
  179. // Register listeners
  180. $listeners = array();
  181. $providers = array();
  182. // Channel listener
  183. $listeners[] = new Reference('security.channel_listener');
  184. // Context serializer listener
  185. if (false === $firewall['stateless']) {
  186. $contextKey = $id;
  187. if (isset($firewall['context'])) {
  188. $contextKey = $firewall['context'];
  189. }
  190. $listeners[] = new Reference($this->createContextListener($container, $contextKey));
  191. }
  192. // Logout listener
  193. if (isset($firewall['logout'])) {
  194. $listenerId = 'security.logout_listener.'.$id;
  195. $listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.logout_listener'));
  196. $listener->addArgument($firewall['logout']['path']);
  197. $listener->addArgument($firewall['logout']['target']);
  198. $listeners[] = new Reference($listenerId);
  199. // add session logout handler
  200. if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) {
  201. $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session')));
  202. }
  203. // add cookie logout handler
  204. if (count($firewall['logout']['delete_cookies']) > 0) {
  205. $cookieHandlerId = 'security.logout.handler.cookie_clearing.'.$id;
  206. $cookieHandler = $container->setDefinition($cookieHandlerId, new DefinitionDecorator('security.logout.handler.cookie_clearing'));
  207. $cookieHandler->addArgument($firewall['logout']['delete_cookies']);
  208. $listener->addMethodCall('addHandler', array(new Reference($cookieHandlerId)));
  209. }
  210. // add custom handlers
  211. foreach ($firewall['logout']['handlers'] as $handlerId) {
  212. $listener->addMethodCall('addHandler', array(new Reference($handlerId)));
  213. }
  214. }
  215. // Authentication listeners
  216. list($authListeners, $providers, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $defaultProvider, $factories);
  217. $listeners = array_merge($listeners, $authListeners);
  218. // Access listener
  219. $listeners[] = new Reference('security.access_listener');
  220. // Switch user listener
  221. if (isset($firewall['switch_user'])) {
  222. $listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider));
  223. }
  224. // Determine default entry point
  225. if (isset($firewall['entry_point'])) {
  226. $defaultEntryPoint = $firewall['entry_point'];
  227. }
  228. // Exception listener
  229. $exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $defaultEntryPoint));
  230. return array($matcher, $listeners, $exceptionListener);
  231. }
  232. protected function createContextListener($container, $contextKey)
  233. {
  234. if (isset($this->contextListeners[$contextKey])) {
  235. return $this->contextListeners[$contextKey];
  236. }
  237. $listenerId = 'security.context_listener.'.count($this->contextListeners);
  238. $listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.context_listener'));
  239. $listener->setArgument(2, $contextKey);
  240. return $this->contextListeners[$contextKey] = $listenerId;
  241. }
  242. protected function createAuthenticationListeners($container, $id, $firewall, $defaultProvider, array $factories)
  243. {
  244. $listeners = array();
  245. $providers = array();
  246. $hasListeners = false;
  247. $defaultEntryPoint = null;
  248. foreach ($this->listenerPositions as $position) {
  249. foreach ($factories[$position] as $factory) {
  250. $key = str_replace('-', '_', $factory->getKey());
  251. if (isset($firewall[$key])) {
  252. $userProvider = isset($firewall[$key]['provider']) ? $this->getUserProviderId($firewall[$key]['provider']) : $defaultProvider;
  253. list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint);
  254. $listeners[] = new Reference($listenerId);
  255. $providers[] = new Reference($provider);
  256. $hasListeners = true;
  257. }
  258. }
  259. }
  260. // Anonymous
  261. if (isset($firewall['anonymous'])) {
  262. $listeners[] = new Reference('security.authentication.listener.anonymous');
  263. $hasListeners = true;
  264. }
  265. if (false === $hasListeners) {
  266. throw new \LogicException(sprintf('No authentication listener registered for pattern "%s".', isset($firewall['pattern']) ? $firewall['pattern'] : ''));
  267. }
  268. return array($listeners, $providers, $defaultEntryPoint);
  269. }
  270. protected function createEncoders($config, ContainerBuilder $container)
  271. {
  272. if (!isset($config['encoders'])) {
  273. return;
  274. }
  275. $encoderMap = array();
  276. foreach ($config['encoders'] as $class => $encoder) {
  277. $encoderMap = $this->createEncoder($encoderMap, $class, $encoder, $container);
  278. }
  279. $container
  280. ->getDefinition('security.encoder_factory.generic')
  281. ->setArguments(array($encoderMap))
  282. ;
  283. }
  284. protected function createEncoder(array $encoderMap, $accountClass, $config, ContainerBuilder $container)
  285. {
  286. // a custom encoder service
  287. if (isset($config['id'])) {
  288. $container
  289. ->getDefinition('security.encoder_factory.generic')
  290. ->addMethodCall('addEncoder', array($accountClass, new Reference($config['id'])))
  291. ;
  292. return $encoderMap;
  293. }
  294. // plaintext encoder
  295. if ('plaintext' === $config['algorithm']) {
  296. $arguments = array();
  297. if (isset($config['ignore_case'])) {
  298. $arguments[0] = $config['ignore_case'];
  299. }
  300. $encoderMap[$accountClass] = array(
  301. 'class' => new Parameter('security.encoder.plain.class'),
  302. 'arguments' => $arguments,
  303. );
  304. return $encoderMap;
  305. }
  306. // message digest encoder
  307. $arguments = array($config['algorithm']);
  308. // add optional arguments
  309. if (isset($config['encode_as_base64'])) {
  310. $arguments[1] = $config['encode_as_base64'];
  311. } else {
  312. $arguments[1] = false;
  313. }
  314. if (isset($config['iterations'])) {
  315. $arguments[2] = $config['iterations'];
  316. } else {
  317. $arguments[2] = 1;
  318. }
  319. $encoderMap[$accountClass] = array(
  320. 'class' => new Parameter('security.encoder.digest.class'),
  321. 'arguments' => $arguments,
  322. );
  323. return $encoderMap;
  324. }
  325. // Parses user providers and returns an array of their ids
  326. protected function createUserProviders($config, ContainerBuilder $container)
  327. {
  328. $providerIds = array();
  329. foreach ($config['providers'] as $name => $provider) {
  330. $id = $this->createUserDaoProvider($name, $provider, $container);
  331. $providerIds[] = $id;
  332. }
  333. return $providerIds;
  334. }
  335. // Parses a <provider> tag and returns the id for the related user provider service
  336. // FIXME: Replace register() calls in this method with DefinitionDecorator
  337. // and move the actual definition to an xml file
  338. protected function createUserDaoProvider($name, $provider, ContainerBuilder $container, $master = true)
  339. {
  340. $name = $this->getUserProviderId(strtolower($name));
  341. // Existing DAO service provider
  342. if (isset($provider['id'])) {
  343. $container->setAlias($name, new Alias($provider['id'], false));
  344. return $provider['id'];
  345. }
  346. // Chain provider
  347. if (count($provider['providers']) > 0) {
  348. // FIXME
  349. throw new \RuntimeException('Not implemented yet.');
  350. }
  351. // Doctrine Entity DAO provider
  352. if (isset($provider['entity'])) {
  353. $container
  354. ->register($name, '%security.user.provider.entity.class%')
  355. ->setPublic(false)
  356. ->setArguments(array(
  357. new Reference('security.user.entity_manager'),
  358. $provider['entity']['class'],
  359. $provider['entity']['property'],
  360. ))
  361. ;
  362. return $name;
  363. }
  364. // Doctrine Document DAO provider
  365. if (isset($provider['document'])) {
  366. $container
  367. ->register($name, '%security.user.provider.document.class%')
  368. ->setPublic(false)
  369. ->setArguments(array(
  370. new Reference('security.user.document_manager'),
  371. $provider['document']['class'],
  372. $provider['document']['property'],
  373. ));
  374. return $name;
  375. }
  376. // In-memory DAO provider
  377. $definition = $container->register($name, '%security.user.provider.in_memory.class%');
  378. $definition->setPublic(false);
  379. foreach ($provider['users'] as $username => $user) {
  380. $userId = $name.'_'.$username;
  381. $container
  382. ->register($userId, 'Symfony\Component\Security\Core\User\User')
  383. ->setArguments(array($username, $user['password'], $user['roles']))
  384. ->setPublic(false)
  385. ;
  386. $definition->addMethodCall('createUser', array(new Reference($userId)));
  387. }
  388. return $name;
  389. }
  390. protected function getUserProviderId($name)
  391. {
  392. return 'security.user.provider.'.$name;
  393. }
  394. protected function createExceptionListener($container, $config, $id, $defaultEntryPoint)
  395. {
  396. $exceptionListenerId = 'security.exception_listener.'.$id;
  397. $listener = $container->setDefinition($exceptionListenerId, new DefinitionDecorator('security.exception_listener'));
  398. $listener->setArgument(2, null === $defaultEntryPoint ? null : new Reference($defaultEntryPoint));
  399. // access denied handler setup
  400. if (isset($config['access_denied_handler'])) {
  401. $listener->setArgument(4, new Reference($config['access_denied_handler']));
  402. } else if (isset($config['access_denied_url'])) {
  403. $listener->setArgument(3, $config['access_denied_url']);
  404. }
  405. return $exceptionListenerId;
  406. }
  407. protected function createSwitchUserListener($container, $id, $config, $defaultProvider)
  408. {
  409. $userProvider = isset($config['provider']) ? $this->getUserProviderId($config['provider']) : $defaultProvider;
  410. $switchUserListenerId = 'security.authentication.switchuser_listener.'.$id;
  411. $listener = $container->setDefinition($switchUserListenerId, new DefinitionDecorator('security.authentication.switchuser_listener'));
  412. $listener->setArgument(1, new Reference($userProvider));
  413. $listener->setArgument(3, $id);
  414. $listener->addArgument($config['parameter']);
  415. $listener->addArgument($config['role']);
  416. return $switchUserListenerId;
  417. }
  418. protected function createRequestMatcher($container, $path = null, $host = null, $methods = null, $ip = null, array $attributes = array())
  419. {
  420. $serialized = serialize(array($path, $host, $methods, $ip, $attributes));
  421. $id = 'security.request_matcher.'.md5($serialized).sha1($serialized);
  422. if (isset($this->requestMatchers[$id])) {
  423. return $this->requestMatchers[$id];
  424. }
  425. // only add arguments that are necessary
  426. $arguments = array($path, $host, $methods, $ip, $attributes);
  427. while (count($arguments) > 0 && !end($arguments)) {
  428. array_pop($arguments);
  429. }
  430. $container
  431. ->register($id, '%security.matcher.class%')
  432. ->setPublic(false)
  433. ->setArguments($arguments)
  434. ;
  435. return $this->requestMatchers[$id] = new Reference($id);
  436. }
  437. protected function createListenerFactories(ContainerBuilder $container, $config)
  438. {
  439. if (null !== $this->factories) {
  440. return $this->factories;
  441. }
  442. // load service templates
  443. $c = new ContainerBuilder();
  444. $parameterBag = $container->getParameterBag();
  445. $loader = new XmlFileLoader($c, new FileLocator(array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config')));
  446. $loader->load('security_factories.xml');
  447. // load user-created listener factories
  448. foreach ($config['factories'] as $factory) {
  449. $loader->load($parameterBag->resolveValue($factory));
  450. }
  451. $tags = $c->findTaggedServiceIds('security.listener.factory');
  452. $factories = array();
  453. foreach ($this->listenerPositions as $position) {
  454. $factories[$position] = array();
  455. }
  456. foreach (array_keys($tags) as $tag) {
  457. $factory = $c->get($tag);
  458. $factories[$factory->getPosition()][] = $factory;
  459. }
  460. return $this->factories = $factories;
  461. }
  462. }