431 lines
19 KiB
PHP
431 lines
19 KiB
PHP
|
<?php
|
||
|
|
||
|
/*
|
||
|
* This file is part of the Symfony package.
|
||
|
*
|
||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||
|
*
|
||
|
* 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\AbstractFactory;
|
||
|
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
|
||
|
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||
|
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||
|
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||
|
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
||
|
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
||
|
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy;
|
||
|
|
||
|
/**
|
||
|
* SecurityExtension configuration structure.
|
||
|
*
|
||
|
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||
|
*/
|
||
|
class MainConfiguration implements ConfigurationInterface
|
||
|
{
|
||
|
/** @internal */
|
||
|
public const STRATEGY_AFFIRMATIVE = 'affirmative';
|
||
|
/** @internal */
|
||
|
public const STRATEGY_CONSENSUS = 'consensus';
|
||
|
/** @internal */
|
||
|
public const STRATEGY_UNANIMOUS = 'unanimous';
|
||
|
/** @internal */
|
||
|
public const STRATEGY_PRIORITY = 'priority';
|
||
|
|
||
|
private array $factories;
|
||
|
private array $userProviderFactories;
|
||
|
|
||
|
/**
|
||
|
* @param array<array-key, AuthenticatorFactoryInterface> $factories
|
||
|
*/
|
||
|
public function __construct(array $factories, array $userProviderFactories)
|
||
|
{
|
||
|
$this->factories = $factories;
|
||
|
$this->userProviderFactories = $userProviderFactories;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generates the configuration tree builder.
|
||
|
*/
|
||
|
public function getConfigTreeBuilder(): TreeBuilder
|
||
|
{
|
||
|
$tb = new TreeBuilder('security');
|
||
|
$rootNode = $tb->getRootNode();
|
||
|
|
||
|
$rootNode
|
||
|
->children()
|
||
|
->scalarNode('access_denied_url')->defaultNull()->example('/foo/error403')->end()
|
||
|
->enumNode('session_fixation_strategy')
|
||
|
->values([SessionAuthenticationStrategy::NONE, SessionAuthenticationStrategy::MIGRATE, SessionAuthenticationStrategy::INVALIDATE])
|
||
|
->defaultValue(SessionAuthenticationStrategy::MIGRATE)
|
||
|
->end()
|
||
|
->booleanNode('hide_user_not_found')->defaultTrue()->end()
|
||
|
->booleanNode('erase_credentials')->defaultTrue()->end()
|
||
|
->booleanNode('enable_authenticator_manager')->defaultTrue()->end()
|
||
|
->arrayNode('access_decision_manager')
|
||
|
->addDefaultsIfNotSet()
|
||
|
->children()
|
||
|
->enumNode('strategy')
|
||
|
->values($this->getAccessDecisionStrategies())
|
||
|
->end()
|
||
|
->scalarNode('service')->end()
|
||
|
->scalarNode('strategy_service')->end()
|
||
|
->booleanNode('allow_if_all_abstain')->defaultFalse()->end()
|
||
|
->booleanNode('allow_if_equal_granted_denied')->defaultTrue()->end()
|
||
|
->end()
|
||
|
->validate()
|
||
|
->ifTrue(function ($v) { return isset($v['strategy'], $v['service']); })
|
||
|
->thenInvalid('"strategy" and "service" cannot be used together.')
|
||
|
->end()
|
||
|
->validate()
|
||
|
->ifTrue(function ($v) { return isset($v['strategy'], $v['strategy_service']); })
|
||
|
->thenInvalid('"strategy" and "strategy_service" cannot be used together.')
|
||
|
->end()
|
||
|
->validate()
|
||
|
->ifTrue(function ($v) { return isset($v['service'], $v['strategy_service']); })
|
||
|
->thenInvalid('"service" and "strategy_service" cannot be used together.')
|
||
|
->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
;
|
||
|
|
||
|
$this->addPasswordHashersSection($rootNode);
|
||
|
$this->addProvidersSection($rootNode);
|
||
|
$this->addFirewallsSection($rootNode, $this->factories);
|
||
|
$this->addAccessControlSection($rootNode);
|
||
|
$this->addRoleHierarchySection($rootNode);
|
||
|
|
||
|
return $tb;
|
||
|
}
|
||
|
|
||
|
private function addRoleHierarchySection(ArrayNodeDefinition $rootNode)
|
||
|
{
|
||
|
$rootNode
|
||
|
->fixXmlConfig('role', 'role_hierarchy')
|
||
|
->children()
|
||
|
->arrayNode('role_hierarchy')
|
||
|
->useAttributeAsKey('id')
|
||
|
->prototype('array')
|
||
|
->performNoDeepMerging()
|
||
|
->beforeNormalization()->ifString()->then(function ($v) { return ['value' => $v]; })->end()
|
||
|
->beforeNormalization()
|
||
|
->ifTrue(function ($v) { return \is_array($v) && isset($v['value']); })
|
||
|
->then(function ($v) { return preg_split('/\s*,\s*/', $v['value']); })
|
||
|
->end()
|
||
|
->prototype('scalar')->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
;
|
||
|
}
|
||
|
|
||
|
private function addAccessControlSection(ArrayNodeDefinition $rootNode)
|
||
|
{
|
||
|
$rootNode
|
||
|
->fixXmlConfig('rule', 'access_control')
|
||
|
->children()
|
||
|
->arrayNode('access_control')
|
||
|
->cannotBeOverwritten()
|
||
|
->prototype('array')
|
||
|
->fixXmlConfig('ip')
|
||
|
->fixXmlConfig('method')
|
||
|
->children()
|
||
|
->scalarNode('requires_channel')->defaultNull()->end()
|
||
|
->scalarNode('path')
|
||
|
->defaultNull()
|
||
|
->info('use the urldecoded format')
|
||
|
->example('^/path to resource/')
|
||
|
->end()
|
||
|
->scalarNode('host')->defaultNull()->end()
|
||
|
->integerNode('port')->defaultNull()->end()
|
||
|
->arrayNode('ips')
|
||
|
->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end()
|
||
|
->prototype('scalar')->end()
|
||
|
->end()
|
||
|
->arrayNode('methods')
|
||
|
->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end()
|
||
|
->prototype('scalar')->end()
|
||
|
->end()
|
||
|
->scalarNode('allow_if')->defaultNull()->end()
|
||
|
->end()
|
||
|
->fixXmlConfig('role')
|
||
|
->children()
|
||
|
->arrayNode('roles')
|
||
|
->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end()
|
||
|
->prototype('scalar')->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param array<array-key, AuthenticatorFactoryInterface> $factories
|
||
|
*/
|
||
|
private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $factories)
|
||
|
{
|
||
|
$firewallNodeBuilder = $rootNode
|
||
|
->fixXmlConfig('firewall')
|
||
|
->children()
|
||
|
->arrayNode('firewalls')
|
||
|
->isRequired()
|
||
|
->requiresAtLeastOneElement()
|
||
|
->disallowNewKeysInSubsequentConfigs()
|
||
|
->useAttributeAsKey('name')
|
||
|
->prototype('array')
|
||
|
->fixXmlConfig('required_badge')
|
||
|
->children()
|
||
|
;
|
||
|
|
||
|
$firewallNodeBuilder
|
||
|
->scalarNode('pattern')->end()
|
||
|
->scalarNode('host')->end()
|
||
|
->arrayNode('methods')
|
||
|
->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end()
|
||
|
->prototype('scalar')->end()
|
||
|
->end()
|
||
|
->booleanNode('security')->defaultTrue()->end()
|
||
|
->scalarNode('user_checker')
|
||
|
->defaultValue('security.user_checker')
|
||
|
->treatNullLike('security.user_checker')
|
||
|
->info('The UserChecker to use when authenticating users in this firewall.')
|
||
|
->end()
|
||
|
->scalarNode('request_matcher')->end()
|
||
|
->scalarNode('access_denied_url')->end()
|
||
|
->scalarNode('access_denied_handler')->end()
|
||
|
->scalarNode('entry_point')
|
||
|
->info(sprintf('An enabled authenticator name or a service id that implements "%s"', AuthenticationEntryPointInterface::class))
|
||
|
->end()
|
||
|
->scalarNode('provider')->end()
|
||
|
->booleanNode('stateless')->defaultFalse()->end()
|
||
|
->booleanNode('lazy')->defaultFalse()->end()
|
||
|
->scalarNode('context')->cannotBeEmpty()->end()
|
||
|
->arrayNode('logout')
|
||
|
->treatTrueLike([])
|
||
|
->canBeUnset()
|
||
|
->children()
|
||
|
->scalarNode('csrf_parameter')->defaultValue('_csrf_token')->end()
|
||
|
->scalarNode('csrf_token_generator')->cannotBeEmpty()->end()
|
||
|
->scalarNode('csrf_token_id')->defaultValue('logout')->end()
|
||
|
->scalarNode('path')->defaultValue('/logout')->end()
|
||
|
->scalarNode('target')->defaultValue('/')->end()
|
||
|
->booleanNode('invalidate_session')->defaultTrue()->end()
|
||
|
->end()
|
||
|
->fixXmlConfig('delete_cookie')
|
||
|
->children()
|
||
|
->arrayNode('delete_cookies')
|
||
|
->normalizeKeys(false)
|
||
|
->beforeNormalization()
|
||
|
->ifTrue(function ($v) { return \is_array($v) && \is_int(key($v)); })
|
||
|
->then(function ($v) { return array_map(function ($v) { return ['name' => $v]; }, $v); })
|
||
|
->end()
|
||
|
->useAttributeAsKey('name')
|
||
|
->prototype('array')
|
||
|
->children()
|
||
|
->scalarNode('path')->defaultNull()->end()
|
||
|
->scalarNode('domain')->defaultNull()->end()
|
||
|
->scalarNode('secure')->defaultFalse()->end()
|
||
|
->scalarNode('samesite')->defaultNull()->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
->arrayNode('switch_user')
|
||
|
->canBeUnset()
|
||
|
->children()
|
||
|
->scalarNode('provider')->end()
|
||
|
->scalarNode('parameter')->defaultValue('_switch_user')->end()
|
||
|
->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
->arrayNode('required_badges')
|
||
|
->info('A list of badges that must be present on the authenticated passport.')
|
||
|
->validate()
|
||
|
->always()
|
||
|
->then(function ($requiredBadges) {
|
||
|
return array_map(function ($requiredBadge) {
|
||
|
if (class_exists($requiredBadge)) {
|
||
|
return $requiredBadge;
|
||
|
}
|
||
|
|
||
|
if (false === strpos($requiredBadge, '\\')) {
|
||
|
$fqcn = 'Symfony\Component\Security\Http\Authenticator\Passport\Badge\\'.$requiredBadge;
|
||
|
if (class_exists($fqcn)) {
|
||
|
return $fqcn;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
throw new InvalidConfigurationException(sprintf('Undefined security Badge class "%s" set in "security.firewall.required_badges".', $requiredBadge));
|
||
|
}, $requiredBadges);
|
||
|
})
|
||
|
->end()
|
||
|
->prototype('scalar')->end()
|
||
|
->end()
|
||
|
;
|
||
|
|
||
|
$abstractFactoryKeys = [];
|
||
|
foreach ($factories as $factory) {
|
||
|
$name = str_replace('-', '_', $factory->getKey());
|
||
|
$factoryNode = $firewallNodeBuilder->arrayNode($name)
|
||
|
->canBeUnset()
|
||
|
;
|
||
|
|
||
|
if ($factory instanceof AbstractFactory) {
|
||
|
$abstractFactoryKeys[] = $name;
|
||
|
}
|
||
|
|
||
|
$factory->addConfiguration($factoryNode);
|
||
|
}
|
||
|
|
||
|
// check for unreachable check paths
|
||
|
$firewallNodeBuilder
|
||
|
->end()
|
||
|
->validate()
|
||
|
->ifTrue(function ($v) {
|
||
|
return true === $v['security'] && isset($v['pattern']) && !isset($v['request_matcher']);
|
||
|
})
|
||
|
->then(function ($firewall) use ($abstractFactoryKeys) {
|
||
|
foreach ($abstractFactoryKeys as $k) {
|
||
|
if (!isset($firewall[$k]['check_path'])) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (str_contains($firewall[$k]['check_path'], '/') && !preg_match('#'.$firewall['pattern'].'#', $firewall[$k]['check_path'])) {
|
||
|
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']));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $firewall;
|
||
|
})
|
||
|
->end()
|
||
|
;
|
||
|
}
|
||
|
|
||
|
private function addProvidersSection(ArrayNodeDefinition $rootNode)
|
||
|
{
|
||
|
$providerNodeBuilder = $rootNode
|
||
|
->fixXmlConfig('provider')
|
||
|
->children()
|
||
|
->arrayNode('providers')
|
||
|
->example([
|
||
|
'my_memory_provider' => [
|
||
|
'memory' => [
|
||
|
'users' => [
|
||
|
'foo' => ['password' => 'foo', 'roles' => 'ROLE_USER'],
|
||
|
'bar' => ['password' => 'bar', 'roles' => '[ROLE_USER, ROLE_ADMIN]'],
|
||
|
],
|
||
|
],
|
||
|
],
|
||
|
'my_entity_provider' => ['entity' => ['class' => 'SecurityBundle:User', 'property' => 'username']],
|
||
|
])
|
||
|
->requiresAtLeastOneElement()
|
||
|
->useAttributeAsKey('name')
|
||
|
->prototype('array')
|
||
|
;
|
||
|
|
||
|
$providerNodeBuilder
|
||
|
->children()
|
||
|
->scalarNode('id')->end()
|
||
|
->arrayNode('chain')
|
||
|
->fixXmlConfig('provider')
|
||
|
->children()
|
||
|
->arrayNode('providers')
|
||
|
->beforeNormalization()
|
||
|
->ifString()
|
||
|
->then(function ($v) { return preg_split('/\s*,\s*/', $v); })
|
||
|
->end()
|
||
|
->prototype('scalar')->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
;
|
||
|
|
||
|
foreach ($this->userProviderFactories as $factory) {
|
||
|
$name = str_replace('-', '_', $factory->getKey());
|
||
|
$factoryNode = $providerNodeBuilder->children()->arrayNode($name)->canBeUnset();
|
||
|
|
||
|
$factory->addConfiguration($factoryNode);
|
||
|
}
|
||
|
|
||
|
$providerNodeBuilder
|
||
|
->validate()
|
||
|
->ifTrue(function ($v) { return \count($v) > 1; })
|
||
|
->thenInvalid('You cannot set multiple provider types for the same provider')
|
||
|
->end()
|
||
|
->validate()
|
||
|
->ifTrue(function ($v) { return 0 === \count($v); })
|
||
|
->thenInvalid('You must set a provider definition for the provider.')
|
||
|
->end()
|
||
|
;
|
||
|
}
|
||
|
|
||
|
private function addPasswordHashersSection(ArrayNodeDefinition $rootNode)
|
||
|
{
|
||
|
$rootNode
|
||
|
->fixXmlConfig('password_hasher')
|
||
|
->children()
|
||
|
->arrayNode('password_hashers')
|
||
|
->example([
|
||
|
'App\Entity\User1' => 'auto',
|
||
|
'App\Entity\User2' => [
|
||
|
'algorithm' => 'auto',
|
||
|
'time_cost' => 8,
|
||
|
'cost' => 13,
|
||
|
],
|
||
|
])
|
||
|
->requiresAtLeastOneElement()
|
||
|
->useAttributeAsKey('class')
|
||
|
->prototype('array')
|
||
|
->canBeUnset()
|
||
|
->performNoDeepMerging()
|
||
|
->beforeNormalization()->ifString()->then(function ($v) { return ['algorithm' => $v]; })->end()
|
||
|
->children()
|
||
|
->scalarNode('algorithm')
|
||
|
->cannotBeEmpty()
|
||
|
->validate()
|
||
|
->ifTrue(function ($v) { return !\is_string($v); })
|
||
|
->thenInvalid('You must provide a string value.')
|
||
|
->end()
|
||
|
->end()
|
||
|
->arrayNode('migrate_from')
|
||
|
->prototype('scalar')->end()
|
||
|
->beforeNormalization()->castToArray()->end()
|
||
|
->end()
|
||
|
->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()
|
||
|
->scalarNode('key_length')->defaultValue(40)->end()
|
||
|
->booleanNode('ignore_case')->defaultFalse()->end()
|
||
|
->booleanNode('encode_as_base64')->defaultTrue()->end()
|
||
|
->scalarNode('iterations')->defaultValue(5000)->end()
|
||
|
->integerNode('cost')
|
||
|
->min(4)
|
||
|
->max(31)
|
||
|
->defaultNull()
|
||
|
->end()
|
||
|
->scalarNode('memory_cost')->defaultNull()->end()
|
||
|
->scalarNode('time_cost')->defaultNull()->end()
|
||
|
->scalarNode('id')->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
->end()
|
||
|
->end();
|
||
|
}
|
||
|
|
||
|
private function getAccessDecisionStrategies(): array
|
||
|
{
|
||
|
return [
|
||
|
self::STRATEGY_AFFIRMATIVE,
|
||
|
self::STRATEGY_CONSENSUS,
|
||
|
self::STRATEGY_UNANIMOUS,
|
||
|
self::STRATEGY_PRIORITY,
|
||
|
];
|
||
|
}
|
||
|
}
|