vendor/symfony/security-bundle/DependencyInjection/MainConfiguration.php line 73

Open in your IDE?
  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\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
  13. use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
  14. use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
  15. use Symfony\Component\Config\Definition\Builder\TreeBuilder;
  16. use Symfony\Component\Config\Definition\ConfigurationInterface;
  17. use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
  18. use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
  19. use Symfony\Component\Security\Http\Event\LogoutEvent;
  20. use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy;
  21. /**
  22. * SecurityExtension configuration structure.
  23. *
  24. * @author Johannes M. Schmitt <schmittjoh@gmail.com>
  25. */
  26. class MainConfiguration implements ConfigurationInterface
  27. {
  28. /** @internal */
  29. public const STRATEGY_AFFIRMATIVE = 'affirmative';
  30. /** @internal */
  31. public const STRATEGY_CONSENSUS = 'consensus';
  32. /** @internal */
  33. public const STRATEGY_UNANIMOUS = 'unanimous';
  34. /** @internal */
  35. public const STRATEGY_PRIORITY = 'priority';
  36. private $factories;
  37. private $userProviderFactories;
  38. /**
  39. * @param array<array-key, SecurityFactoryInterface|AuthenticatorFactoryInterface> $factories
  40. */
  41. public function __construct(array $factories, array $userProviderFactories)
  42. {
  43. if (\is_array(current($factories))) {
  44. trigger_deprecation('symfony/security-bundle', '5.4', 'Passing an array of arrays as 1st argument to "%s" is deprecated, pass a sorted array of factories instead.', __METHOD__);
  45. $factories = array_merge(...array_values($factories));
  46. }
  47. $this->factories = $factories;
  48. $this->userProviderFactories = $userProviderFactories;
  49. }
  50. /**
  51. * Generates the configuration tree builder.
  52. *
  53. * @return TreeBuilder
  54. */
  55. public function getConfigTreeBuilder()
  56. {
  57. $tb = new TreeBuilder('security');
  58. $rootNode = $tb->getRootNode();
  59. $rootNode
  60. ->beforeNormalization()
  61. ->ifTrue(function ($v) {
  62. if ($v['encoders'] ?? false) {
  63. trigger_deprecation('symfony/security-bundle', '5.3', 'The child node "encoders" at path "security" is deprecated, use "password_hashers" instead.');
  64. return true;
  65. }
  66. return $v['password_hashers'] ?? false;
  67. })
  68. ->then(function ($v) {
  69. $v['password_hashers'] = array_merge($v['password_hashers'] ?? [], $v['encoders'] ?? []);
  70. $v['encoders'] = $v['password_hashers'];
  71. return $v;
  72. })
  73. ->end()
  74. ->children()
  75. ->scalarNode('access_denied_url')->defaultNull()->example('/foo/error403')->end()
  76. ->enumNode('session_fixation_strategy')
  77. ->values([SessionAuthenticationStrategy::NONE, SessionAuthenticationStrategy::MIGRATE, SessionAuthenticationStrategy::INVALIDATE])
  78. ->defaultValue(SessionAuthenticationStrategy::MIGRATE)
  79. ->end()
  80. ->booleanNode('hide_user_not_found')->defaultTrue()->end()
  81. ->booleanNode('always_authenticate_before_granting')
  82. ->defaultFalse()
  83. ->setDeprecated('symfony/security-bundle', '5.4')
  84. ->end()
  85. ->booleanNode('erase_credentials')->defaultTrue()->end()
  86. ->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()
  87. ->arrayNode('access_decision_manager')
  88. ->addDefaultsIfNotSet()
  89. ->children()
  90. ->enumNode('strategy')
  91. ->values($this->getAccessDecisionStrategies())
  92. ->end()
  93. ->scalarNode('service')->end()
  94. ->scalarNode('strategy_service')->end()
  95. ->booleanNode('allow_if_all_abstain')->defaultFalse()->end()
  96. ->booleanNode('allow_if_equal_granted_denied')->defaultTrue()->end()
  97. ->end()
  98. ->validate()
  99. ->ifTrue(function ($v) { return isset($v['strategy'], $v['service']); })
  100. ->thenInvalid('"strategy" and "service" cannot be used together.')
  101. ->end()
  102. ->validate()
  103. ->ifTrue(function ($v) { return isset($v['strategy'], $v['strategy_service']); })
  104. ->thenInvalid('"strategy" and "strategy_service" cannot be used together.')
  105. ->end()
  106. ->validate()
  107. ->ifTrue(function ($v) { return isset($v['service'], $v['strategy_service']); })
  108. ->thenInvalid('"service" and "strategy_service" cannot be used together.')
  109. ->end()
  110. ->end()
  111. ->end()
  112. ;
  113. $this->addEncodersSection($rootNode);
  114. $this->addPasswordHashersSection($rootNode);
  115. $this->addProvidersSection($rootNode);
  116. $this->addFirewallsSection($rootNode, $this->factories);
  117. $this->addAccessControlSection($rootNode);
  118. $this->addRoleHierarchySection($rootNode);
  119. return $tb;
  120. }
  121. private function addRoleHierarchySection(ArrayNodeDefinition $rootNode)
  122. {
  123. $rootNode
  124. ->fixXmlConfig('role', 'role_hierarchy')
  125. ->children()
  126. ->arrayNode('role_hierarchy')
  127. ->useAttributeAsKey('id')
  128. ->prototype('array')
  129. ->performNoDeepMerging()
  130. ->beforeNormalization()->ifString()->then(function ($v) { return ['value' => $v]; })->end()
  131. ->beforeNormalization()
  132. ->ifTrue(function ($v) { return \is_array($v) && isset($v['value']); })
  133. ->then(function ($v) { return preg_split('/\s*,\s*/', $v['value']); })
  134. ->end()
  135. ->prototype('scalar')->end()
  136. ->end()
  137. ->end()
  138. ->end()
  139. ;
  140. }
  141. private function addAccessControlSection(ArrayNodeDefinition $rootNode)
  142. {
  143. $rootNode
  144. ->fixXmlConfig('rule', 'access_control')
  145. ->children()
  146. ->arrayNode('access_control')
  147. ->cannotBeOverwritten()
  148. ->prototype('array')
  149. ->fixXmlConfig('ip')
  150. ->fixXmlConfig('method')
  151. ->children()
  152. ->scalarNode('requires_channel')->defaultNull()->end()
  153. ->scalarNode('path')
  154. ->defaultNull()
  155. ->info('use the urldecoded format')
  156. ->example('^/path to resource/')
  157. ->end()
  158. ->scalarNode('host')->defaultNull()->end()
  159. ->integerNode('port')->defaultNull()->end()
  160. ->arrayNode('ips')
  161. ->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end()
  162. ->prototype('scalar')->end()
  163. ->end()
  164. ->arrayNode('methods')
  165. ->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end()
  166. ->prototype('scalar')->end()
  167. ->end()
  168. ->scalarNode('allow_if')->defaultNull()->end()
  169. ->end()
  170. ->fixXmlConfig('role')
  171. ->children()
  172. ->arrayNode('roles')
  173. ->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end()
  174. ->prototype('scalar')->end()
  175. ->end()
  176. ->end()
  177. ->end()
  178. ->end()
  179. ->end()
  180. ;
  181. }
  182. /**
  183. * @param array<array-key, SecurityFactoryInterface|AuthenticatorFactoryInterface> $factories
  184. */
  185. private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $factories)
  186. {
  187. $firewallNodeBuilder = $rootNode
  188. ->fixXmlConfig('firewall')
  189. ->children()
  190. ->arrayNode('firewalls')
  191. ->isRequired()
  192. ->requiresAtLeastOneElement()
  193. ->disallowNewKeysInSubsequentConfigs()
  194. ->useAttributeAsKey('name')
  195. ->prototype('array')
  196. ->fixXmlConfig('required_badge')
  197. ->children()
  198. ;
  199. $firewallNodeBuilder
  200. ->scalarNode('pattern')->end()
  201. ->scalarNode('host')->end()
  202. ->arrayNode('methods')
  203. ->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end()
  204. ->prototype('scalar')->end()
  205. ->end()
  206. ->booleanNode('security')->defaultTrue()->end()
  207. ->scalarNode('user_checker')
  208. ->defaultValue('security.user_checker')
  209. ->treatNullLike('security.user_checker')
  210. ->info('The UserChecker to use when authenticating users in this firewall.')
  211. ->end()
  212. ->scalarNode('request_matcher')->end()
  213. ->scalarNode('access_denied_url')->end()
  214. ->scalarNode('access_denied_handler')->end()
  215. ->scalarNode('entry_point')
  216. ->info(sprintf('An enabled authenticator name or a service id that implements "%s"', AuthenticationEntryPointInterface::class))
  217. ->end()
  218. ->scalarNode('provider')->end()
  219. ->booleanNode('stateless')->defaultFalse()->end()
  220. ->booleanNode('lazy')->defaultFalse()->end()
  221. ->scalarNode('context')->cannotBeEmpty()->end()
  222. ->arrayNode('logout')
  223. ->treatTrueLike([])
  224. ->canBeUnset()
  225. ->children()
  226. ->scalarNode('csrf_parameter')->defaultValue('_csrf_token')->end()
  227. ->scalarNode('csrf_token_generator')->cannotBeEmpty()->end()
  228. ->scalarNode('csrf_token_id')->defaultValue('logout')->end()
  229. ->scalarNode('path')->defaultValue('/logout')->end()
  230. ->scalarNode('target')->defaultValue('/')->end()
  231. ->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()
  232. ->booleanNode('invalidate_session')->defaultTrue()->end()
  233. ->end()
  234. ->fixXmlConfig('delete_cookie')
  235. ->children()
  236. ->arrayNode('delete_cookies')
  237. ->normalizeKeys(false)
  238. ->beforeNormalization()
  239. ->ifTrue(function ($v) { return \is_array($v) && \is_int(key($v)); })
  240. ->then(function ($v) { return array_map(function ($v) { return ['name' => $v]; }, $v); })
  241. ->end()
  242. ->useAttributeAsKey('name')
  243. ->prototype('array')
  244. ->children()
  245. ->scalarNode('path')->defaultNull()->end()
  246. ->scalarNode('domain')->defaultNull()->end()
  247. ->scalarNode('secure')->defaultFalse()->end()
  248. ->scalarNode('samesite')->defaultNull()->end()
  249. ->end()
  250. ->end()
  251. ->end()
  252. ->end()
  253. ->fixXmlConfig('handler')
  254. ->children()
  255. ->arrayNode('handlers')
  256. ->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()
  257. ->end()
  258. ->end()
  259. ->end()
  260. ->arrayNode('switch_user')
  261. ->canBeUnset()
  262. ->children()
  263. ->scalarNode('provider')->end()
  264. ->scalarNode('parameter')->defaultValue('_switch_user')->end()
  265. ->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end()
  266. ->end()
  267. ->end()
  268. ->arrayNode('required_badges')
  269. ->info('A list of badges that must be present on the authenticated passport.')
  270. ->validate()
  271. ->always()
  272. ->then(function ($requiredBadges) {
  273. return array_map(function ($requiredBadge) {
  274. if (class_exists($requiredBadge)) {
  275. return $requiredBadge;
  276. }
  277. if (false === strpos($requiredBadge, '\\')) {
  278. $fqcn = 'Symfony\Component\Security\Http\Authenticator\Passport\Badge\\'.$requiredBadge;
  279. if (class_exists($fqcn)) {
  280. return $fqcn;
  281. }
  282. }
  283. throw new InvalidConfigurationException(sprintf('Undefined security Badge class "%s" set in "security.firewall.required_badges".', $requiredBadge));
  284. }, $requiredBadges);
  285. })
  286. ->end()
  287. ->prototype('scalar')->end()
  288. ->end()
  289. ;
  290. $abstractFactoryKeys = [];
  291. foreach ($factories as $factory) {
  292. $name = str_replace('-', '_', $factory->getKey());
  293. $factoryNode = $firewallNodeBuilder->arrayNode($name)
  294. ->canBeUnset()
  295. ;
  296. if ($factory instanceof AbstractFactory) {
  297. $abstractFactoryKeys[] = $name;
  298. }
  299. $factory->addConfiguration($factoryNode);
  300. }
  301. // check for unreachable check paths
  302. $firewallNodeBuilder
  303. ->end()
  304. ->validate()
  305. ->ifTrue(function ($v) {
  306. return true === $v['security'] && isset($v['pattern']) && !isset($v['request_matcher']);
  307. })
  308. ->then(function ($firewall) use ($abstractFactoryKeys) {
  309. foreach ($abstractFactoryKeys as $k) {
  310. if (!isset($firewall[$k]['check_path'])) {
  311. continue;
  312. }
  313. if (str_contains($firewall[$k]['check_path'], '/') && !preg_match('#'.$firewall['pattern'].'#', $firewall[$k]['check_path'])) {
  314. 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']));
  315. }
  316. }
  317. return $firewall;
  318. })
  319. ->end()
  320. ;
  321. }
  322. private function addProvidersSection(ArrayNodeDefinition $rootNode)
  323. {
  324. $providerNodeBuilder = $rootNode
  325. ->fixXmlConfig('provider')
  326. ->children()
  327. ->arrayNode('providers')
  328. ->example([
  329. 'my_memory_provider' => [
  330. 'memory' => [
  331. 'users' => [
  332. 'foo' => ['password' => 'foo', 'roles' => 'ROLE_USER'],
  333. 'bar' => ['password' => 'bar', 'roles' => '[ROLE_USER, ROLE_ADMIN]'],
  334. ],
  335. ],
  336. ],
  337. 'my_entity_provider' => ['entity' => ['class' => 'SecurityBundle:User', 'property' => 'username']],
  338. ])
  339. ->requiresAtLeastOneElement()
  340. ->useAttributeAsKey('name')
  341. ->prototype('array')
  342. ;
  343. $providerNodeBuilder
  344. ->children()
  345. ->scalarNode('id')->end()
  346. ->arrayNode('chain')
  347. ->fixXmlConfig('provider')
  348. ->children()
  349. ->arrayNode('providers')
  350. ->beforeNormalization()
  351. ->ifString()
  352. ->then(function ($v) { return preg_split('/\s*,\s*/', $v); })
  353. ->end()
  354. ->prototype('scalar')->end()
  355. ->end()
  356. ->end()
  357. ->end()
  358. ->end()
  359. ;
  360. foreach ($this->userProviderFactories as $factory) {
  361. $name = str_replace('-', '_', $factory->getKey());
  362. $factoryNode = $providerNodeBuilder->children()->arrayNode($name)->canBeUnset();
  363. $factory->addConfiguration($factoryNode);
  364. }
  365. $providerNodeBuilder
  366. ->validate()
  367. ->ifTrue(function ($v) { return \count($v) > 1; })
  368. ->thenInvalid('You cannot set multiple provider types for the same provider')
  369. ->end()
  370. ->validate()
  371. ->ifTrue(function ($v) { return 0 === \count($v); })
  372. ->thenInvalid('You must set a provider definition for the provider.')
  373. ->end()
  374. ;
  375. }
  376. private function addEncodersSection(ArrayNodeDefinition $rootNode)
  377. {
  378. $rootNode
  379. ->fixXmlConfig('encoder')
  380. ->children()
  381. ->arrayNode('encoders')
  382. ->example([
  383. 'App\Entity\User1' => 'auto',
  384. 'App\Entity\User2' => [
  385. 'algorithm' => 'auto',
  386. 'time_cost' => 8,
  387. 'cost' => 13,
  388. ],
  389. ])
  390. ->requiresAtLeastOneElement()
  391. ->useAttributeAsKey('class')
  392. ->prototype('array')
  393. ->canBeUnset()
  394. ->performNoDeepMerging()
  395. ->beforeNormalization()->ifString()->then(function ($v) { return ['algorithm' => $v]; })->end()
  396. ->children()
  397. ->scalarNode('algorithm')
  398. ->cannotBeEmpty()
  399. ->validate()
  400. ->ifTrue(function ($v) { return !\is_string($v); })
  401. ->thenInvalid('You must provide a string value.')
  402. ->end()
  403. ->end()
  404. ->arrayNode('migrate_from')
  405. ->prototype('scalar')->end()
  406. ->beforeNormalization()->castToArray()->end()
  407. ->end()
  408. ->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()
  409. ->scalarNode('key_length')->defaultValue(40)->end()
  410. ->booleanNode('ignore_case')->defaultFalse()->end()
  411. ->booleanNode('encode_as_base64')->defaultTrue()->end()
  412. ->scalarNode('iterations')->defaultValue(5000)->end()
  413. ->integerNode('cost')
  414. ->min(4)
  415. ->max(31)
  416. ->defaultNull()
  417. ->end()
  418. ->scalarNode('memory_cost')->defaultNull()->end()
  419. ->scalarNode('time_cost')->defaultNull()->end()
  420. ->scalarNode('id')->end()
  421. ->end()
  422. ->end()
  423. ->end()
  424. ->end()
  425. ;
  426. }
  427. private function addPasswordHashersSection(ArrayNodeDefinition $rootNode)
  428. {
  429. $rootNode
  430. ->fixXmlConfig('password_hasher')
  431. ->children()
  432. ->arrayNode('password_hashers')
  433. ->example([
  434. 'App\Entity\User1' => 'auto',
  435. 'App\Entity\User2' => [
  436. 'algorithm' => 'auto',
  437. 'time_cost' => 8,
  438. 'cost' => 13,
  439. ],
  440. ])
  441. ->requiresAtLeastOneElement()
  442. ->useAttributeAsKey('class')
  443. ->prototype('array')
  444. ->canBeUnset()
  445. ->performNoDeepMerging()
  446. ->beforeNormalization()->ifString()->then(function ($v) { return ['algorithm' => $v]; })->end()
  447. ->children()
  448. ->scalarNode('algorithm')
  449. ->cannotBeEmpty()
  450. ->validate()
  451. ->ifTrue(function ($v) { return !\is_string($v); })
  452. ->thenInvalid('You must provide a string value.')
  453. ->end()
  454. ->end()
  455. ->arrayNode('migrate_from')
  456. ->prototype('scalar')->end()
  457. ->beforeNormalization()->castToArray()->end()
  458. ->end()
  459. ->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()
  460. ->scalarNode('key_length')->defaultValue(40)->end()
  461. ->booleanNode('ignore_case')->defaultFalse()->end()
  462. ->booleanNode('encode_as_base64')->defaultTrue()->end()
  463. ->scalarNode('iterations')->defaultValue(5000)->end()
  464. ->integerNode('cost')
  465. ->min(4)
  466. ->max(31)
  467. ->defaultNull()
  468. ->end()
  469. ->scalarNode('memory_cost')->defaultNull()->end()
  470. ->scalarNode('time_cost')->defaultNull()->end()
  471. ->scalarNode('id')->end()
  472. ->end()
  473. ->end()
  474. ->end()
  475. ->end();
  476. }
  477. private function getAccessDecisionStrategies(): array
  478. {
  479. return [
  480. self::STRATEGY_AFFIRMATIVE,
  481. self::STRATEGY_CONSENSUS,
  482. self::STRATEGY_UNANIMOUS,
  483. self::STRATEGY_PRIORITY,
  484. ];
  485. }
  486. }