I try to change the access control decision strategy in a Symfony project but it doesn't seem to work.
I have the following in my security.yaml:
security:
access_decision_manager:
strategy: unanimous
allow_if_all_abstain: false
access_control:
- { path: ^/dev/test, roles: [COMPLEX_TEST, ROLE_OTHER] }
I need both COMPLEX_TEST and ROLE_OTHER to be granted for the route to be accessible (COMPLEX_TEST is tested by a custom role voter).
But when I try to access the route, only the COMPLEXT_TEST voter is called, and that's because it allows the access and the strategy is still affirmative.
I can say this because when debugging the code, I can see that the value in Symfony\Component\Security\Core\Authorization\AccessDecisionManager is always affirmative, no matter what I set in the security.yaml.
For now I've created a compiler pass that forces the strategy to unanimous, but it's kind of a hack:
class FixAccessDecisionManagerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if ($container->hasDefinition('security.access.decision_manager')) {
$definition = $container->getDefinition('security.access.decision_manager');
$definition->setArgument('$strategy', AccessDecisionManager::STRATEGY_UNANIMOUS);
}
}
}
With the compiler pass I can see that the value in Symfony\Component\Security\Core\Authorization\AccessDecisionManager is correcly set to unanimous and the access control works as expected.
Do you see what I am missing? Or is it a bug in Symfony? My Symfony version is 4.4.7.
Thank you for your time.
Related
I meet an unusual problem.
We have a form_login (based on FOS user-bundle). And now we want to change it to hslavich/OneloginSamlBundle for saml auth. But we want to save ability to select an auth method by changing environment vars in kubernetes deployment.
We use k8s on prod with pre-build images (implements "bin/console cache:warmup" in composer scripts for generating cache).
I'm implemented an environment variable for switch needed config.
Than I generate a switcher like this:
return static function (ContainerConfigurator $container) {
$isSamlEnabled = getenv('IS_AUTH_SAML_ENABLE');
if($isSamlEnabled === true) {
$container->import('security_provider_configs/saml.yml');
}
else {
$container->import('security_provider_configs/ldap.yml');
}
};
But this solution use fixed variable IS_AUTH_SAML_ENABLE, which was is in builded image and can't be changed in kubernetes deployment.
We can add new APP_ENV stage, for difference prod-form and prod-saml, we can build two images like 'v2.123-form' and 'v2.123-saml'. But this will brake all CI/CD in our company. It's very difficult.
Do you know any methods to switch auth method by change env variable?
security.yml like this:
security:
providers:
form_usr:
id: my_service.provider.user
saml_provider:
entity:
class: MyService\UserModel
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt|error)|css|images|js)/
security: false
main:
pattern: ^/
saml:
provider: saml_provider
user_factory: user_saml_factory
username_attribute: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
persist_user: true
check_path: /saml/acs
login_path: /saml/login
form_login:
provider: form_usr
default_target_path: about
always_use_default_target_path: true
logout:
target: /login
anonymous: true
Disclaimer: I'm not completely certain that this will work, but this case demands some testing on your part. ;o) Please report back if it indeed does work.
As Nico Haase correctly pointed out, your env vars are resolved at cache/compile-time. However, this is a poster case for APP_ENV (which is usually dev on dev systems and prod on production), you could add/use prod_ldap and prod_saml instead of just prod, which absolutely can have implications on your application. (switching a user from one to the other will not work without some hassle or at least re-login) - see https://symfony.com/doc/4.1/configuration/environments.html for some more information about adding more environments. The documentation is for symfony 4.1 so please don't blindly follow the examples. Stuff has changed, but the general idea should still be viable.
For that to work, you would have to adapt config/bundles.php, and possibly src/Kernel.php and maybe even more things, and you probably have to copy some of the env-specific configs ...
Since all caching and container-building is done depending on APP_ENV and the results are written to a APP_ENV-specific cache location, the containers as well as the caches and sessions would reside in different locations - you'd have the same code base and the same project directory but different cache and config dirs. Unless your application is extremely sophisticated and sensitive to this, this should work.
Please note, that depending on how you're changing your APP_ENV, this might absolutely not work. If it's set by the webserver, I'm confident it should.
Please also note, that to put your system live, you will have to do both bin/console cache:clear --env=prod_saml as well as bin/console cache:clear --env=prod_ldap, composer will run the one in .env(.local) automatically - if you even run composer - but you only can have one at the time. You could extend the composer.json to run both cache:clear commands as a post-something script.
Thanks to #Jakumi for an answer! But this solution have some troubles in our CI/CD.
I solved my problem. It takes to add a private parameter to Kernel:
final class Kernel extends SymfonyKernel
{
...
private $cacheSuffix = '';
and fill it in constructor:
public function __construct(string $environment, bool $debug)
{
parent::__construct($environment, $debug);
$useSaml = getenv('SAML_ENABLE') === 'true';
$this->cacheSuffix = $useSaml ? '/saml' : '';
}
After that we need to replace Kernel->getCacheDir by custom:
public function getCacheDir(): string
{
return $this->getProjectDir() . '/var/cache/' . $this->getEnvironment() . $this->cacheSuffix;
}
So, we can to warmup cache for both auth methods, for example in composer.json:
...
"scripts": {
"auto-scripts": [
"export SAML_ENABLE=false && bin/console cache:warmup",
"export SAML_ENABLE=true && bin/console cache:warmup"
],
...
I'm trying to refactor some Symfony 3 code to Symfony 4.
I am getting the following error when attempting to log:
The "monolog.logger.db" service or alias has been removed or inlined
when the container was compiled. You should either make it public, or
stop using the conta iner directly and use dependency injection
instead.
My logging code:
$logger = $container->get('monolog.logger.db');
$logger->info('Import command triggered');
Monolog config:
monolog:
channels: ['db']
handlers:
db:
channels: ['db']
type: service
id: app.monolog.db_handler
app.monolog.db_handler config (Note, I tried public: true here and it had no affect:
app.monolog.db_handler:
class: App\Util\MonologDBHandler
arguments: ['#doctrine.orm.entity_manager']
How can I get this wired up correctly in Symfony 4?
By default all services in Symfony 4 are private (and is the recommended pratice) so you need to "inject" in each Controller each needed service (personally I use a custom CommonControllerServiceClass).
You can also create a public service "alias" to continue accessing the service as you did, but it's not the best pratice to follow (also because I guess you will have many other services to fix).
mylogger.db:
alias: monolog.logger.db
public: true
then you can get the service from the container:
$logger = $container->get('mylogger.db');
Alister's answer is a good start, but you can utilise service arguments binding instead of creating a new service for each logger:
services:
_defaults:
autowire: true
bind:
$databaseLogger: '#monolog.logger.db'
Then just change the argument name in your class:
// in App\Util\MonologDBHandler.php
use Psr\Log\LoggerInterface;
public function __construct(LoggerInterface $databaseLogger = null) {...}
It appears that App\Util\MonologDBHandler may be the only thing that is actively using monolog.logger.db - via a container->get('...') call. (If not, you will want to use this technique to tag the specific sort of logger into more services).
You would be better to allow the framework to build the app.monolog.db_handler service itself, and use the container to help to build it. Normally, to inject a logger service, you will just need to type-hint it:
// in App\Util\MonologDBHandler.php
use Psr\Log\LoggerInterface;
public function __construct(LoggerInterface $logger = null) {...}
However, that will, by default, setup with the default #logger, so you need to add an extra hint in the service definition of the handler that you want a different type of logger:
services:
App\Log\CustomLogger:
arguments: ['#logger']
tags:
- { name: monolog.logger, channel: db }
Now, the logger in CustomLogger should be what you had previously known as monolog.logger.db.
You can also alias a different interface (similar to how the LoggerInterface is aliased to inject '#logger') to the allow for the tagging.
I use voters that I set up based on this guide (I know it's a Sonata guide but it uses no Sonata code).
Now the voters are working fine, they grant deny as needed. One voter service definition looks like this:
services:
acme_account.security.authorization.organisation_voter:
class: %acme_account.security.authorization.organisation_voter.class%
arguments: [#service_container]
public: false
tags:
- { name: security.voter }
Now my problem is that even though the voter returns correct grants, in some cases some default ACL handler denies permission. This is in the logs:
security.DEBUG: No ACL found for the object identity. Voting to deny access. [] []
Since I want to enforce the denies coming from the voters I have set the security.access_decision_manager.strategy to unanimous. But because of the default handler this way the permissions are denied.
Now of course I could configure and start using the ACLs but it would be an overkill in this application that's why I choose the voters.
Is there any way to disable this default behaviour?
Here's a workaround for it, not sure if this is the best way but it works.
The object and security identity retrieval strategy services needed to be overwritten with noop implementations.
services.yml
security.acl.object_identity_retrieval_strategy:
class: Acme\UserBundle\Acl\ObjectIdentityRetrievalStrategy
security.acl.security_identity_retrieval_strategy:
class: Acme\UserBundle\Acl\SecurityIdentityRetrievalStrategy
Acme\UserBundle\Acl\ObjectIdentityRetrievalStrategy.php
<?php
namespace Acme\UserBundle\Acl;
use Symfony\Component\Security\Acl\Model\ObjectIdentityRetrievalStrategyInterface;
class ObjectIdentityRetrievalStrategy implements ObjectIdentityRetrievalStrategyInterface
{
public function getObjectIdentity($domainObject)
{
}
}
Acme\UserBundle\Acl\SecurityIdentityRetrievalStrategy.php
<?php
namespace Acme\UserBundle\Acl;
use Symfony\Component\Security\Acl\Model\SecurityIdentityRetrievalStrategyInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
class SecurityIdentityRetrievalStrategy implements SecurityIdentityRetrievalStrategyInterface
{
public function getSecurityIdentities(TokenInterface $token)
{
}
}
I want to override the setTargetPath from the default ExceptionListener (documentation). But I need additional service to handle this.
In my opinion there is only the way to override the service definition, copy it to my service definitions and create an own constructor, but I don't like this approach.
Is there any other way to do this?
As for answer, if you are using form_login type, you can set it to a constant route where the redirect after login should happen. Config
You should set these 2 keys:
always_use_default_target_path: true
default_target_path: /route_name_for_redirect
Or option B, you use a success handler service, where you simply returns a RedirectResponse
I have found another approach using a compiler pass, following steps are necessary:
Create your own class extending ExceptionListener and add for your dependencies a method
Create a compiler pass containing something like the following:
class WebCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('security.exception_listener');
$definition->addMethodCall('yourmethod', [ new Reference('your dependency') ]);
}
}
Register your compiler pass
Overwrite the exception listener class parameter:
security.exception_listener.class: AcmeBundle\EventListener\SecurityExceptionListener
I'm using the standard authentication mechanism of Symfony2 and I want to let the user use either his username or email to login, but I can't find out why it's not working. I've tested the repository class and it works as expected. I've followed this how-to.
Here's my user provider class:
<?php
namespace My\UserBundle\Entity;
use Doctrine\ORM\EntityRepository ,
Symfony\Component\Security\Core\User\UserProviderInterface ,
Symfony\Component\Security\Core\User\UserInterface;
/**
* UserRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class UserRepository extends EntityRepository implements UserProviderInterface
{
function loadUserByUsername($username)
{
$qb = $this->createQueryBuilder('u') ;
return
$qb->select('u')
->where(
$qb->expr()->orx(
$qb->expr()->like('u.username' ,':username') ,
$qb->expr()->like('u.email' ,':username')
)
)
//->andWhere($qb->expr()->eq('u.enabled' ,'true') )
->setParameters(array('username' =>$username ) )
->getQuery()
->getResult() ;
}
function refreshUser(UserInterface $user)
{
return $this->loadUserByUsername($user->getUsername() );
}
function supportsClass($class)
{
return $class === 'My\UserBundle\Entity\User';
}
}
I propose a more simple approach that only requires to edit security.yml file.
You must to create two diferent security providers but both using the same User class. The first has username as property and the second has email as property.
Then you create a chain_provider that includes the two providers and use it in your firewall section
security:
providers:
chain_provider:
chain:
providers: [db_username, db_email]
db_username:
entity:
class: MyUserBundle:User
property: username
db_email:
entity:
class: MyUserBundle:User
property: email
firewalls:
default:
anonymous: ~
provider: chain_provider
form_login:
login_path: /login
check_path: /login
I don't know if this approach is a clean practice, but is fast, simple and it works ok.
well guys the thing is in my security.yml i had this
providers:
main:
entity: { class: My\UserBundle\Entity\User ,property : username}
so i had to take off that parameter property :username
Taking off the property: username lets the UserProviderInterface load the user as expected when it logs in, but does not call the refreshUser() method as expected. I put in checks to see if it gets called but it doesn't.
The class that reloads the user on each access is ContextListener::refreshUser(TokenInterface $token) method. In this the interface iterates through the UserProviders and calls the refreshUser that first returns a non-null user.
I could make sure of this because, in the original load, I combine all different entities to make one SQL call instead of 7. And when the user reloads, it calls 7 times.
Also the method EntityUserProvider::refreshUser() doesn't call the repository's method and instead reloads from the database directly.
Your provider class is correct, and you are correct that the problem is in security.yml, however, your solution is incorrect.
According to the documentation, your security.yml file should look like this:
security:
# ...
providers:
administrators:
entity: { class: MyUserBundle:User }
Notice that the class is defined as the Bundle, not the direct class.
The way you have your code right now, symfony is completly ignoring your repository class until you define your security.yml correctly. And as #Anand pointed out, just removing the property does not invoke refreshUser. However, it looks like if you are using your own Repository, you do not need to define the property (since it's being defined in your query).