SecurityExtension.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. <?php
  2. namespace Symfony\Bundle\FrameworkBundle\DependencyInjection;
  3. use Symfony\Component\DependencyInjection\Parameter;
  4. use Symfony\Component\DependencyInjection\Extension\Extension;
  5. use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
  6. use Symfony\Component\DependencyInjection\Resource\FileResource;
  7. use Symfony\Component\DependencyInjection\ContainerBuilder;
  8. use Symfony\Component\DependencyInjection\Reference;
  9. use Symfony\Component\DependencyInjection\Definition;
  10. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
  11. use Symfony\Component\HttpFoundation\RequestMatcher;
  12. /*
  13. * This file is part of the Symfony framework.
  14. *
  15. * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
  16. *
  17. * This source file is subject to the MIT license that is bundled
  18. * with this source code in the file LICENSE.
  19. */
  20. /**
  21. * SecurityExtension.
  22. *
  23. * @author Fabien Potencier <fabien.potencier@symfony-project.com>
  24. */
  25. class SecurityExtension extends Extension
  26. {
  27. /**
  28. * Loads the web configuration.
  29. *
  30. * @param array $config An array of configuration settings
  31. * @param ContainerBuilder $container A ContainerBuilder instance
  32. */
  33. public function configLoad($config, ContainerBuilder $container)
  34. {
  35. if (!$container->hasDefinition('security.context')) {
  36. $loader = new XmlFileLoader($container, array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config'));
  37. $loader->load('security.xml');
  38. }
  39. if (isset($config['access-denied-url'])) {
  40. $container->setParameter('security.access_denied.url', $config['access-denied-url']);
  41. }
  42. $this->createFirewalls($config, $container);
  43. $this->createAuthorization($config, $container);
  44. $this->createRoleHierarchy($config, $container);
  45. return $container;
  46. }
  47. protected function createRoleHierarchy($config, ContainerBuilder $container)
  48. {
  49. $roles = array();
  50. if (isset($config['role_hierarchy'])) {
  51. $roles = $config['role_hierarchy'];
  52. } elseif (isset($config['role-hierarchy'])) {
  53. $roles = $config['role-hierarchy'];
  54. }
  55. if (isset($roles['role']) && is_int(key($roles['role']))) {
  56. $roles = $roles['role'];
  57. }
  58. $hierarchy = array();
  59. foreach ($roles as $id => $role) {
  60. if (is_array($role) && isset($role['id'])) {
  61. $id = $role['id'];
  62. }
  63. $value = $role;
  64. if (is_array($role) && isset($role['value'])) {
  65. $value = $role['value'];
  66. }
  67. $hierarchy[$id] = is_array($value) ? $value : preg_split('/\s*,\s*/', $value);
  68. }
  69. $container->setParameter('security.role_hierarchy.roles', $hierarchy);
  70. $container->remove('security.access.simple_role_voter');
  71. $container->getDefinition('security.access.role_hierarchy_voter')->addTag('security.voter');
  72. }
  73. protected function createAuthorization($config, ContainerBuilder $container)
  74. {
  75. $rules = array();
  76. if (isset($config['access_control'])) {
  77. $rules = $config['access_control'];
  78. } elseif (isset($config['access-control'])) {
  79. $rules = $config['access-control'];
  80. }
  81. if (isset($rules['rule']) && is_array($rules['rule'])) {
  82. $rules = $rules['rule'];
  83. }
  84. foreach ($rules as $i => $access) {
  85. $roles = isset($access['role']) ? (is_array($access['role']) ? $access['role'] : preg_split('/\s*,\s*/', $access['role'])) : array();
  86. $channel = null;
  87. if (isset($access['requires-channel'])) {
  88. $channel = $access['requires-channel'];
  89. } elseif (isset($access['requires_channel'])) {
  90. $channel = $access['requires_channel'];
  91. }
  92. // matcher
  93. $id = 'security.matcher.url.'.$i;
  94. $definition = $container->register($id, '%security.matcher.class%');
  95. $definition->setPublic(false);
  96. if (isset($access['path'])) {
  97. $definition->addMethodCall('matchPath', array(is_array($access['path']) ? $access['path']['pattern'] : $access['path']));
  98. }
  99. $attributes = $this->fixConfig($access, 'attribute');
  100. foreach ($attributes as $key => $attribute) {
  101. if (isset($attribute['key'])) {
  102. $key = $attribute['key'];
  103. }
  104. $definition->addMethodCall('matchAttribute', array($key, $attribute['pattern']));
  105. }
  106. $container->getDefinition('security.access_map')->addMethodCall('add', array(new Reference($id), $roles, $channel));
  107. }
  108. }
  109. protected function createFirewalls($config, ContainerBuilder $container)
  110. {
  111. $providerIds = $this->createUserProviders($config, $container);
  112. if (!$firewalls = $this->fixConfig($config, 'firewall')) {
  113. return;
  114. }
  115. // make the ContextListener aware of the configured user providers
  116. $definition = $container->getDefinition('security.context_listener');
  117. $arguments = $definition->getArguments();
  118. $userProviders = array();
  119. foreach (array_keys($providerIds) as $userProviderId) {
  120. $userProviders[] = new Reference($userProviderId);
  121. }
  122. $arguments[1] = $userProviders;
  123. $definition->setArguments($arguments);
  124. // load service templates
  125. $c = new ContainerBuilder($container->getParameterBag());
  126. $loader = new XmlFileLoader($c, array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config'));
  127. $loader->load('security_templates.xml');
  128. foreach ($this->fixConfig($config, 'template') as $template) {
  129. $loader->load($c->getParameterBag()->resolveValue($template));
  130. }
  131. $container->merge($c);
  132. // load firewall map
  133. $map = $container->getDefinition('security.firewall.map');
  134. foreach ($firewalls as $firewall) {
  135. list($matcher, $listeners, $exceptionListener) = $this->createFirewall($container, $firewall, $providerIds);
  136. $map->addMethodCall('add', array($matcher, $listeners, $exceptionListener));
  137. }
  138. // remove all service templates
  139. foreach ($c->getServiceIds() as $id) {
  140. $container->remove($id);
  141. }
  142. }
  143. protected function createFirewall(ContainerBuilder $container, $firewall, $providerIds)
  144. {
  145. // unique id for this firewall
  146. $id = md5(serialize($firewall));
  147. // Matcher
  148. $i = 0;
  149. $matcher = null;
  150. if (isset($firewall['pattern'])) {
  151. $id = 'security.matcher.map'.$id.'.'.++$i;
  152. $matcher = $container
  153. ->register($id, '%security.matcher.class%')
  154. ->setPublic(false)
  155. ->addMethodCall('matchPath', array($firewall['pattern']))
  156. ;
  157. $matcher = new Reference($id);
  158. }
  159. // Security disabled?
  160. if (isset($firewall['security']) && !$firewall['security']) {
  161. return array($matcher, array(), null);
  162. }
  163. // Provider id (take the first registered provider if none defined)
  164. if (isset($firewall['provider'])) {
  165. $defaultProvider = $this->getUserProviderId($firewall['provider']);
  166. } else {
  167. if (!$providerIds) {
  168. throw new \InvalidArgumentException('You must provide at least one authentication provider.');
  169. }
  170. $keys = array_keys($providerIds);
  171. $defaultProvider = current($keys);
  172. }
  173. // Register listeners
  174. $listeners = array();
  175. $providers = array();
  176. // Channel listener
  177. $listeners[] = new Reference('security.channel_listener');
  178. // Context serializer listener
  179. if (!isset($firewall['stateless']) || !$firewall['stateless']) {
  180. $listeners[] = new Reference('security.context_listener');
  181. }
  182. // Logout listener
  183. if (array_key_exists('logout', $firewall)) {
  184. $listenerId = 'security.logout_listener.'.$id;
  185. $listener = $container->setDefinition($listenerId, clone $container->getDefinition('security.logout_listener'));
  186. $listeners[] = new Reference($listenerId);
  187. $arguments = $listener->getArguments();
  188. if (isset($firewall['logout']['path'])) {
  189. $arguments[1] = $firewall['logout']['path'];
  190. }
  191. if (isset($firewall['logout']['target'])) {
  192. $arguments[2] = $firewall['logout']['target'];
  193. }
  194. $listener->setArguments($arguments);
  195. if (!isset($firewall['stateless']) || !$firewall['stateless']) {
  196. $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session')));
  197. }
  198. if (count($cookies = $this->fixConfig($firewall['logout'], 'cookie')) > 0) {
  199. $cookieHandlerId = 'security.logout.handler.cookie_clearing.'.$id;
  200. $cookieHandler = $container->setDefinition($cookieHandlerId, clone $container->getDefinition('security.logout.handler.cookie_clearing'));
  201. $cookieHandler->setArguments(array($cookies));
  202. $listener->addMethodCall('addHandler', array(new Reference($cookieHandlerId)));
  203. }
  204. }
  205. // Authentication listeners
  206. list($authListeners, $providers, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $defaultProvider, $providerIds);
  207. $listeners = array_merge($listeners, $authListeners);
  208. // Access listener
  209. $listeners[] = new Reference($this->createAccessListener($container, $id, $providers));
  210. // Switch user listener
  211. if (array_key_exists('switch_user', $firewall)) {
  212. $firewall['switch-user'] = $firewall['switch_user'];
  213. }
  214. if (array_key_exists('switch-user', $firewall)) {
  215. $listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch-user'], $defaultProvider));
  216. }
  217. // Exception listener
  218. $exceptionListener = new Reference($this->createExceptionListener($container, $id, $defaultEntryPoint));
  219. return array($matcher, $listeners, $exceptionListener);
  220. }
  221. protected function createAuthenticationListeners($container, $id, $firewall, $defaultProvider, $providerIds)
  222. {
  223. $listeners = array();
  224. $providers = array();
  225. $hasListeners = false;
  226. $defaultEntryPoint = null;
  227. $positions = array('pre_auth', 'form', 'http');
  228. $tags = $container->findTaggedServiceIds('security.listener.factory');
  229. $factories = array();
  230. foreach ($positions as $position) {
  231. $factories[$position] = array();
  232. }
  233. foreach (array_keys($tags) as $tag) {
  234. $factory = $container->get($tag);
  235. $factories[$factory->getPosition()][] = $factory;
  236. }
  237. foreach ($positions as $position) {
  238. foreach ($factories[$position] as $factory) {
  239. $key = $factory->getKey();
  240. $keybis = str_replace('-', '_', $key);
  241. if (array_key_exists($keybis, $firewall)) {
  242. $firewall[$key] = $firewall[$keybis];
  243. }
  244. if (array_key_exists($key, $firewall)) {
  245. $userProvider = isset($firewall[$key]['provider']) ? $this->getUserProviderId($firewall[$key]['provider']) : $defaultProvider;
  246. list($provider, $listener, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $providerIds, $defaultEntryPoint);
  247. $listeners[] = new Reference($listener);
  248. $providers[] = new Reference($provider);
  249. $hasListeners = true;
  250. }
  251. }
  252. }
  253. // Anonymous
  254. if (array_key_exists('anonymous', $firewall)) {
  255. $listeners[] = new Reference('security.authentication.listener.anonymous');
  256. $hasListeners = true;
  257. }
  258. if (false === $hasListeners) {
  259. throw new \LogicException(sprintf('No authentication listener registered for pattern "%s".', isset($firewall['pattern']) ? $firewall['pattern'] : ''));
  260. }
  261. return array($listeners, $providers, $defaultEntryPoint);
  262. }
  263. // Parses user providers and returns an array of their ids
  264. protected function createUserProviders($config, ContainerBuilder $container)
  265. {
  266. $providers = $this->fixConfig($config, 'provider');
  267. if (!$providers) {
  268. return array();
  269. }
  270. $providerIds = array();
  271. foreach ($providers as $name => $provider) {
  272. list($id, $encoder) = $this->createUserDaoProvider($name, $provider, $container);
  273. if (isset($providerIds[$id])) {
  274. throw new \RuntimeException(sprintf('Provider names must be unique. Duplicate entry for %s.', $id));
  275. }
  276. $providerIds[$id] = $encoder;
  277. }
  278. return $providerIds;
  279. }
  280. // Parses a <provider> tag and returns the id for the related user provider service
  281. protected function createUserDaoProvider($name, $provider, ContainerBuilder $container, $master = true)
  282. {
  283. // encoder
  284. $encoder = 'plain';
  285. if (isset($provider['password-encoder'])) {
  286. $encoder = $provider['password-encoder'];
  287. } elseif (isset($provider['password_encoder'])) {
  288. $encoder = $provider['password_encoder'];
  289. }
  290. if (isset($provider['name'])) {
  291. $name = $provider['name'];
  292. }
  293. if (!$name) {
  294. throw new \RuntimeException('You must define a name for each user provider.');
  295. }
  296. $name = $this->getUserProviderId(strtolower($name));
  297. // Existing DAO service provider
  298. if (isset($provider['id'])) {
  299. $container->setAlias($name, $provider['id']);
  300. return array($name, $encoder);
  301. }
  302. // Chain provider
  303. if (isset($provider['provider'])) {
  304. // FIXME
  305. throw new \RuntimeException('Not implemented yet.');
  306. }
  307. // Doctrine Entity DAO provider
  308. if (isset($provider['entity'])) {
  309. $container
  310. ->register($name, '%security.user.provider.entity.class%')
  311. ->setPublic(false)
  312. ->setArguments(array(
  313. new Reference('security.user.entity_manager'),
  314. $provider['entity']['class'],
  315. isset($provider['entity']['property']) ? $provider['entity']['property'] : null,
  316. ));
  317. return array($name, $encoder);
  318. }
  319. // Doctrine Document DAO provider
  320. if (isset($provider['document'])) {
  321. $container
  322. ->register($name, '%security.user.provider.document.class%')
  323. ->setPublic(false)
  324. ->setArguments(array(
  325. new Reference('security.user.document_manager'),
  326. $provider['document']['class'],
  327. isset($provider['document']['property']) ? $provider['document']['property'] : null,
  328. ));
  329. return array($name, $encoder);
  330. }
  331. // In-memory DAO provider
  332. $definition = $container->register($name, '%security.user.provider.in_memory.class%');
  333. $definition->setPublic(false);
  334. foreach ($this->fixConfig($provider, 'user') as $username => $user) {
  335. if (isset($user['name'])) {
  336. $username = $user['name'];
  337. }
  338. if (!array_key_exists('password', $user)) {
  339. // if no password is provided explicitly, it means that
  340. // the user will be used with OpenID, X.509 certificates, ...
  341. // Let's generate a random password just to be sure this
  342. // won't be used accidentally with other authentication schemes.
  343. // If you want an empty password, just say so explicitly
  344. $user['password'] = uniqid();
  345. }
  346. if (!isset($user['roles'])) {
  347. $user['roles'] = array();
  348. } else {
  349. $user['roles'] = is_array($user['roles']) ? $user['roles'] : preg_split('/\s*,\s*/', $user['roles']);
  350. }
  351. $userId = $name.'_'.md5(serialize(array($username, $user['password'], $user['roles'])));
  352. $container
  353. ->register($userId, 'Symfony\Component\Security\User\User')
  354. ->setArguments(array($username, $user['password'], $user['roles']))
  355. ->setPublic(false)
  356. ;
  357. $definition->addMethodCall('createUser', array(new Reference($userId)));
  358. }
  359. return array($name, $encoder);
  360. }
  361. protected function getUserProviderId($name)
  362. {
  363. return 'security.authentication.provider.'.$name;
  364. }
  365. protected function createAccessListener($container, $id, $providers)
  366. {
  367. // Authentication manager
  368. $authManager = 'security.authentication.manager.'.$id;
  369. $container
  370. ->register($authManager, '%security.authentication.manager.class%')
  371. ->addArgument($providers)
  372. ->setPublic(false)
  373. ;
  374. // Access listener
  375. $listenerId = 'security.access_listener.'.$id;
  376. $listener = $container->setDefinition($listenerId, clone $container->getDefinition('security.access_listener'));
  377. $arguments = $listener->getArguments();
  378. $arguments[3] = new Reference($authManager);
  379. $listener->setArguments($arguments);
  380. return $listenerId;
  381. }
  382. protected function createExceptionListener($container, $id, $defaultEntryPoint)
  383. {
  384. $exceptionListenerId = 'security.exception_listener.'.$id;
  385. $listener = $container->setDefinition($exceptionListenerId, clone $container->getDefinition('security.exception_listener'));
  386. $arguments = $listener->getArguments();
  387. $arguments[2] = null === $defaultEntryPoint ? null : new Reference($defaultEntryPoint);
  388. $listener->setArguments($arguments);
  389. return $exceptionListenerId;
  390. }
  391. protected function createSwitchUserListener($container, $id, $config, $defaultProvider)
  392. {
  393. $userProvider = isset($config['provider']) ? $this->getUserProviderId($config['provider']) : $defaultProvider;
  394. $switchUserListenerId = 'security.authentication.switchuser_listener.'.$id;
  395. $listener = $container->setDefinition($switchUserListenerId, clone $container->getDefinition('security.authentication.switchuser_listener'));
  396. $arguments = $listener->getArguments();
  397. $arguments[1] = new Reference($userProvider);
  398. $listener->setArguments($arguments);
  399. if (isset($config['role'])) {
  400. $container->setParameter('security.authentication.switchuser.role', $config['role']);
  401. }
  402. if (isset($config['parameter'])) {
  403. $container->setParameter('security.authentication.switchuser.parameter', $config['parameter']);
  404. }
  405. return $switchUserListenerId;
  406. }
  407. public function aclLoad(array $config, ContainerBuilder $container)
  408. {
  409. if (!$container->hasDefinition('security.acl')) {
  410. $loader = new XmlFileLoader($container, array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config'));
  411. $loader->load('security_acl.xml');
  412. }
  413. if (isset($config['connection'])) {
  414. $container->setAlias(sprintf('doctrine.dbal.%s_connection', $config['connection']), 'security.acl.dbal.connection');
  415. }
  416. if (isset($config['cache'])) {
  417. $container->setAlias('security.acl.cache', sprintf('security.acl.cache.%s', $config['cache']));
  418. } else {
  419. $container->remove('security.acl.cache.doctrine');
  420. $container->removeAlias('security.acl.cache.doctrine.cache_impl');
  421. }
  422. }
  423. /**
  424. * Returns the base path for the XSD files.
  425. *
  426. * @return string The XSD base path
  427. */
  428. public function getXsdValidationBasePath()
  429. {
  430. return __DIR__.'/../Resources/config/schema';
  431. }
  432. public function getNamespace()
  433. {
  434. return 'http://www.symfony-project.org/schema/dic/security';
  435. }
  436. public function getAlias()
  437. {
  438. return 'security';
  439. }
  440. protected function fixConfig($config, $key)
  441. {
  442. $values = array();
  443. if (isset($config[$key.'s'])) {
  444. $values = $config[$key.'s'];
  445. } elseif (isset($config[$key])) {
  446. if (is_string($config[$key]) || !is_int(key($config[$key]))) {
  447. // only one
  448. $values = array($config[$key]);
  449. } else {
  450. $values = $config[$key];
  451. }
  452. }
  453. return $values;
  454. }
  455. }