MainConfiguration.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.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\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory;
  12. use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
  13. use Symfony\Component\Config\Definition\Builder\TreeBuilder;
  14. use Symfony\Component\Config\Definition\ConfigurationInterface;
  15. use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
  16. use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
  17. use Symfony\Component\Security\Http\Event\LogoutEvent;
  18. use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy;
  19. /**
  20. * SecurityExtension configuration structure.
  21. *
  22. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  23. */
  24. class MainConfiguration implements ConfigurationInterface
  25. {
  26. private $factories;
  27. private $userProviderFactories;
  28. public function __construct(array $factories, array $userProviderFactories)
  29. {
  30. $this->factories = $factories;
  31. $this->userProviderFactories = $userProviderFactories;
  32. }
  33. /**
  34. * Generates the configuration tree builder.
  35. *
  36. * @return TreeBuilder The tree builder
  37. */
  38. public function getConfigTreeBuilder()
  39. {
  40. $tb = new TreeBuilder('security');
  41. $rootNode = $tb->getRootNode();
  42. $rootNode
  43. ->beforeNormalization()
  44. ->ifTrue(function ($v) {
  45. if (!isset($v['access_decision_manager'])) {
  46. return true;
  47. }
  48. if (!isset($v['access_decision_manager']['strategy']) && !isset($v['access_decision_manager']['service'])) {
  49. return true;
  50. }
  51. return false;
  52. })
  53. ->then(function ($v) {
  54. $v['access_decision_manager']['strategy'] = AccessDecisionManager::STRATEGY_AFFIRMATIVE;
  55. return $v;
  56. })
  57. ->end()
  58. ->children()
  59. ->scalarNode('access_denied_url')->defaultNull()->example('/foo/error403')->end()
  60. ->enumNode('session_fixation_strategy')
  61. ->values([SessionAuthenticationStrategy::NONE, SessionAuthenticationStrategy::MIGRATE, SessionAuthenticationStrategy::INVALIDATE])
  62. ->defaultValue(SessionAuthenticationStrategy::MIGRATE)
  63. ->end()
  64. ->booleanNode('hide_user_not_found')->defaultTrue()->end()
  65. ->booleanNode('always_authenticate_before_granting')->defaultFalse()->end()
  66. ->booleanNode('erase_credentials')->defaultTrue()->end()
  67. ->booleanNode('enable_authenticator_manager')->defaultFalse()->info('Enables the new Symfony Security system based on Authenticators, all used authenticators must support this before enabling this.')->end()
  68. ->arrayNode('access_decision_manager')
  69. ->addDefaultsIfNotSet()
  70. ->children()
  71. ->enumNode('strategy')
  72. ->values($this->getAccessDecisionStrategies())
  73. ->end()
  74. ->scalarNode('service')->end()
  75. ->booleanNode('allow_if_all_abstain')->defaultFalse()->end()
  76. ->booleanNode('allow_if_equal_granted_denied')->defaultTrue()->end()
  77. ->end()
  78. ->validate()
  79. ->ifTrue(function ($v) { return isset($v['strategy']) && isset($v['service']); })
  80. ->thenInvalid('"strategy" and "service" cannot be used together.')
  81. ->end()
  82. ->end()
  83. ->end()
  84. ;
  85. $this->addEncodersSection($rootNode);
  86. $this->addProvidersSection($rootNode);
  87. $this->addFirewallsSection($rootNode, $this->factories);
  88. $this->addAccessControlSection($rootNode);
  89. $this->addRoleHierarchySection($rootNode);
  90. return $tb;
  91. }
  92. private function addRoleHierarchySection(ArrayNodeDefinition $rootNode)
  93. {
  94. $rootNode
  95. ->fixXmlConfig('role', 'role_hierarchy')
  96. ->children()
  97. ->arrayNode('role_hierarchy')
  98. ->useAttributeAsKey('id')
  99. ->prototype('array')
  100. ->performNoDeepMerging()
  101. ->beforeNormalization()->ifString()->then(function ($v) { return ['value' => $v]; })->end()
  102. ->beforeNormalization()
  103. ->ifTrue(function ($v) { return \is_array($v) && isset($v['value']); })
  104. ->then(function ($v) { return preg_split('/\s*,\s*/', $v['value']); })
  105. ->end()
  106. ->prototype('scalar')->end()
  107. ->end()
  108. ->end()
  109. ->end()
  110. ;
  111. }
  112. private function addAccessControlSection(ArrayNodeDefinition $rootNode)
  113. {
  114. $rootNode
  115. ->fixXmlConfig('rule', 'access_control')
  116. ->children()
  117. ->arrayNode('access_control')
  118. ->cannotBeOverwritten()
  119. ->prototype('array')
  120. ->fixXmlConfig('ip')
  121. ->fixXmlConfig('method')
  122. ->children()
  123. ->scalarNode('requires_channel')->defaultNull()->end()
  124. ->scalarNode('path')
  125. ->defaultNull()
  126. ->info('use the urldecoded format')
  127. ->example('^/path to resource/')
  128. ->end()
  129. ->scalarNode('host')->defaultNull()->end()
  130. ->integerNode('port')->defaultNull()->end()
  131. ->arrayNode('ips')
  132. ->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end()
  133. ->prototype('scalar')->end()
  134. ->end()
  135. ->arrayNode('methods')
  136. ->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end()
  137. ->prototype('scalar')->end()
  138. ->end()
  139. ->scalarNode('allow_if')->defaultNull()->end()
  140. ->end()
  141. ->fixXmlConfig('role')
  142. ->children()
  143. ->arrayNode('roles')
  144. ->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end()
  145. ->prototype('scalar')->end()
  146. ->end()
  147. ->end()
  148. ->end()
  149. ->end()
  150. ->end()
  151. ;
  152. }
  153. private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $factories)
  154. {
  155. $firewallNodeBuilder = $rootNode
  156. ->fixXmlConfig('firewall')
  157. ->children()
  158. ->arrayNode('firewalls')
  159. ->isRequired()
  160. ->requiresAtLeastOneElement()
  161. ->disallowNewKeysInSubsequentConfigs()
  162. ->useAttributeAsKey('name')
  163. ->prototype('array')
  164. ->children()
  165. ;
  166. $firewallNodeBuilder
  167. ->scalarNode('pattern')->end()
  168. ->scalarNode('host')->end()
  169. ->arrayNode('methods')
  170. ->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end()
  171. ->prototype('scalar')->end()
  172. ->end()
  173. ->booleanNode('security')->defaultTrue()->end()
  174. ->scalarNode('user_checker')
  175. ->defaultValue('security.user_checker')
  176. ->treatNullLike('security.user_checker')
  177. ->info('The UserChecker to use when authenticating users in this firewall.')
  178. ->end()
  179. ->scalarNode('request_matcher')->end()
  180. ->scalarNode('access_denied_url')->end()
  181. ->scalarNode('access_denied_handler')->end()
  182. ->scalarNode('entry_point')
  183. ->info(sprintf('An enabled authenticator name or a service id that implements "%s"', AuthenticationEntryPointInterface::class))
  184. ->end()
  185. ->scalarNode('provider')->end()
  186. ->booleanNode('stateless')->defaultFalse()->end()
  187. ->booleanNode('lazy')->defaultFalse()->end()
  188. ->scalarNode('context')->cannotBeEmpty()->end()
  189. ->arrayNode('logout')
  190. ->treatTrueLike([])
  191. ->canBeUnset()
  192. ->children()
  193. ->scalarNode('csrf_parameter')->defaultValue('_csrf_token')->end()
  194. ->scalarNode('csrf_token_generator')->cannotBeEmpty()->end()
  195. ->scalarNode('csrf_token_id')->defaultValue('logout')->end()
  196. ->scalarNode('path')->defaultValue('/logout')->end()
  197. ->scalarNode('target')->defaultValue('/')->end()
  198. ->scalarNode('success_handler')->setDeprecated('symfony/security-bundle', '5.1', sprintf('The "%%node%%" at path "%%path%%" is deprecated, register a listener on the "%s" event instead.', LogoutEvent::class))->end()
  199. ->booleanNode('invalidate_session')->defaultTrue()->end()
  200. ->end()
  201. ->fixXmlConfig('delete_cookie')
  202. ->children()
  203. ->arrayNode('delete_cookies')
  204. ->normalizeKeys(false)
  205. ->beforeNormalization()
  206. ->ifTrue(function ($v) { return \is_array($v) && \is_int(key($v)); })
  207. ->then(function ($v) { return array_map(function ($v) { return ['name' => $v]; }, $v); })
  208. ->end()
  209. ->useAttributeAsKey('name')
  210. ->prototype('array')
  211. ->children()
  212. ->scalarNode('path')->defaultNull()->end()
  213. ->scalarNode('domain')->defaultNull()->end()
  214. ->scalarNode('secure')->defaultFalse()->end()
  215. ->scalarNode('samesite')->defaultNull()->end()
  216. ->end()
  217. ->end()
  218. ->end()
  219. ->end()
  220. ->fixXmlConfig('handler')
  221. ->children()
  222. ->arrayNode('handlers')
  223. ->prototype('scalar')->setDeprecated('symfony/security-bundle', '5.1', sprintf('The "%%node%%" at path "%%path%%" is deprecated, register a listener on the "%s" event instead.', LogoutEvent::class))->end()
  224. ->end()
  225. ->end()
  226. ->end()
  227. ->arrayNode('switch_user')
  228. ->canBeUnset()
  229. ->children()
  230. ->scalarNode('provider')->end()
  231. ->scalarNode('parameter')->defaultValue('_switch_user')->end()
  232. ->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end()
  233. ->end()
  234. ->end()
  235. ;
  236. $abstractFactoryKeys = [];
  237. foreach ($factories as $factoriesAtPosition) {
  238. foreach ($factoriesAtPosition as $factory) {
  239. $name = str_replace('-', '_', $factory->getKey());
  240. $factoryNode = $firewallNodeBuilder->arrayNode($name)
  241. ->canBeUnset()
  242. ;
  243. if ($factory instanceof AbstractFactory) {
  244. $abstractFactoryKeys[] = $name;
  245. }
  246. $factory->addConfiguration($factoryNode);
  247. }
  248. }
  249. // check for unreachable check paths
  250. $firewallNodeBuilder
  251. ->end()
  252. ->validate()
  253. ->ifTrue(function ($v) {
  254. return true === $v['security'] && isset($v['pattern']) && !isset($v['request_matcher']);
  255. })
  256. ->then(function ($firewall) use ($abstractFactoryKeys) {
  257. foreach ($abstractFactoryKeys as $k) {
  258. if (!isset($firewall[$k]['check_path'])) {
  259. continue;
  260. }
  261. if (false !== strpos($firewall[$k]['check_path'], '/') && !preg_match('#'.$firewall['pattern'].'#', $firewall[$k]['check_path'])) {
  262. throw new \LogicException(sprintf('The check_path "%s" for login method "%s" is not matched by the firewall pattern "%s".', $firewall[$k]['check_path'], $k, $firewall['pattern']));
  263. }
  264. }
  265. return $firewall;
  266. })
  267. ->end()
  268. ;
  269. }
  270. private function addProvidersSection(ArrayNodeDefinition $rootNode)
  271. {
  272. $providerNodeBuilder = $rootNode
  273. ->fixXmlConfig('provider')
  274. ->children()
  275. ->arrayNode('providers')
  276. ->example([
  277. 'my_memory_provider' => [
  278. 'memory' => [
  279. 'users' => [
  280. 'foo' => ['password' => 'foo', 'roles' => 'ROLE_USER'],
  281. 'bar' => ['password' => 'bar', 'roles' => '[ROLE_USER, ROLE_ADMIN]'],
  282. ],
  283. ],
  284. ],
  285. 'my_entity_provider' => ['entity' => ['class' => 'SecurityBundle:User', 'property' => 'username']],
  286. ])
  287. ->requiresAtLeastOneElement()
  288. ->useAttributeAsKey('name')
  289. ->prototype('array')
  290. ;
  291. $providerNodeBuilder
  292. ->children()
  293. ->scalarNode('id')->end()
  294. ->arrayNode('chain')
  295. ->fixXmlConfig('provider')
  296. ->children()
  297. ->arrayNode('providers')
  298. ->beforeNormalization()
  299. ->ifString()
  300. ->then(function ($v) { return preg_split('/\s*,\s*/', $v); })
  301. ->end()
  302. ->prototype('scalar')->end()
  303. ->end()
  304. ->end()
  305. ->end()
  306. ->end()
  307. ;
  308. foreach ($this->userProviderFactories as $factory) {
  309. $name = str_replace('-', '_', $factory->getKey());
  310. $factoryNode = $providerNodeBuilder->children()->arrayNode($name)->canBeUnset();
  311. $factory->addConfiguration($factoryNode);
  312. }
  313. $providerNodeBuilder
  314. ->validate()
  315. ->ifTrue(function ($v) { return \count($v) > 1; })
  316. ->thenInvalid('You cannot set multiple provider types for the same provider')
  317. ->end()
  318. ->validate()
  319. ->ifTrue(function ($v) { return 0 === \count($v); })
  320. ->thenInvalid('You must set a provider definition for the provider.')
  321. ->end()
  322. ;
  323. }
  324. private function addEncodersSection(ArrayNodeDefinition $rootNode)
  325. {
  326. $rootNode
  327. ->fixXmlConfig('encoder')
  328. ->children()
  329. ->arrayNode('encoders')
  330. ->example([
  331. 'App\Entity\User1' => 'auto',
  332. 'App\Entity\User2' => [
  333. 'algorithm' => 'auto',
  334. 'time_cost' => 8,
  335. 'cost' => 13,
  336. ],
  337. ])
  338. ->requiresAtLeastOneElement()
  339. ->useAttributeAsKey('class')
  340. ->prototype('array')
  341. ->canBeUnset()
  342. ->performNoDeepMerging()
  343. ->beforeNormalization()->ifString()->then(function ($v) { return ['algorithm' => $v]; })->end()
  344. ->children()
  345. ->scalarNode('algorithm')
  346. ->cannotBeEmpty()
  347. ->validate()
  348. ->ifTrue(function ($v) { return !\is_string($v); })
  349. ->thenInvalid('You must provide a string value.')
  350. ->end()
  351. ->end()
  352. ->arrayNode('migrate_from')
  353. ->prototype('scalar')->end()
  354. ->beforeNormalization()->castToArray()->end()
  355. ->end()
  356. ->scalarNode('hash_algorithm')->info('Name of hashing algorithm for PBKDF2 (i.e. sha256, sha512, etc..) See hash_algos() for a list of supported algorithms.')->defaultValue('sha512')->end()
  357. ->scalarNode('key_length')->defaultValue(40)->end()
  358. ->booleanNode('ignore_case')->defaultFalse()->end()
  359. ->booleanNode('encode_as_base64')->defaultTrue()->end()
  360. ->scalarNode('iterations')->defaultValue(5000)->end()
  361. ->integerNode('cost')
  362. ->min(4)
  363. ->max(31)
  364. ->defaultNull()
  365. ->end()
  366. ->scalarNode('memory_cost')->defaultNull()->end()
  367. ->scalarNode('time_cost')->defaultNull()->end()
  368. ->scalarNode('id')->end()
  369. ->end()
  370. ->end()
  371. ->end()
  372. ->end()
  373. ;
  374. }
  375. private function getAccessDecisionStrategies()
  376. {
  377. $strategies = [
  378. AccessDecisionManager::STRATEGY_AFFIRMATIVE,
  379. AccessDecisionManager::STRATEGY_CONSENSUS,
  380. AccessDecisionManager::STRATEGY_UNANIMOUS,
  381. ];
  382. if (\defined(AccessDecisionManager::class.'::STRATEGY_PRIORITY')) {
  383. $strategies[] = AccessDecisionManager::STRATEGY_PRIORITY;
  384. }
  385. return $strategies;
  386. }
  387. }