How can I swtich authorization method by ENV variable? - symfony

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"
],
...

Related

ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface service does not exists

Hey Im trying API Platform with Symfony 6.0 (and PHP 8)
Everything was going alright until I needed to make a DataPersister so I can encrypt the user password before saving it
I literally copied the example in the docs (here https://api-platform.com/docs/core/data-persisters/#decorating-the-built-in-data-persisters) since my entity is actually called User:
<?php
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\Entity\User;
final class UserDataPersister implements ContextAwareDataPersisterInterface
{
private $decorated;
public function __construct(ContextAwareDataPersisterInterface $decorated)
{
$this->decorated = $decorated;
}
public function supports($data, array $context = []): bool
{
return $this->decorated->supports($data, $context);
}
public function persist($data, array $context = [])
{
$result = $this->decorated->persist($data, $context);
return $result;
}
public function remove($data, array $context = [])
{
return $this->decorated->remove($data, $context);
}
}
I just removed the mailer parts cause what Im trying to do has nothing to do with that. Other than that, it is exactly equal to the example
But it wont work. I get this error when I try to persist:
Cannot autowire service "App\DataPersister\UserDataPersister": argument "$decorated" of method "__construct()" references interface "ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface" but no such service exists. Try changing the type-hint to "ApiPlatform\Core\DataPersister\DataPersisterInterface" instead.
I tried doing what the error suggests but it seems to throw the framework in some endless loop or something cause I get a memory error. And in any case, I need a ContextAwareDataPersisterInterface
Am I doing something wrong or missing something here? Or this a bug? The docs says:
"If service autowiring and autoconfiguration are enabled (they are by default), you are done!"
They are both enabled in services.yaml:
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
I works if I explicity define the service in services.yaml:
App\DataPersister\UserDataPersister:
bind:
$decorated: '#api_platform.doctrine.orm.data_persister'
edit: sorry, the documentation actually says we have to do that, I missed it. My bad.
Problem solved

Symfony access decision manager strategy is always affirmative

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.

Deprecation: Doctrine\ORM\Mapping\UnderscoreNamingStrategy without making it number aware is deprecated

I'm using Symfony 4.3.8 and I can't find any information about thoses deprecations :
User Deprecated: Creating Doctrine\ORM\Mapping\UnderscoreNamingStrategy without making it number aware is deprecated and will be removed in Doctrine ORM 3.0.
Creating Doctrine\ORM\Mapping\UnderscoreNamingStrategy without making it number aware is deprecated and will be removed in Doctrine ORM 3.0.
I searched in stacktrace and found this :
class UnderscoreNamingStrategy implements NamingStrategy
{
private const DEFAULT_PATTERN = '/(?<=[a-z])([A-Z])/';
private const NUMBER_AWARE_PATTERN = '/(?<=[a-z0-9])([A-Z])/';
/**
* Underscore naming strategy construct.
*
* #param int $case CASE_LOWER | CASE_UPPER
*/
public function __construct($case = CASE_LOWER, bool $numberAware = false)
{
if (! $numberAware) {
#trigger_error(
'Creating ' . self::class . ' without making it number aware is deprecated and will be removed in Doctrine ORM 3.0.',
E_USER_DEPRECATED
);
}
$this->case = $case;
$this->pattern = $numberAware ? self::NUMBER_AWARE_PATTERN : self::DEFAULT_PATTERN;
}
In this class, the constructor is always called without params, so $numberAware is always false.
This class is called in file which has been auto generated by the Symfony Dependency Injection, so I can't "edit" it ...
I thought maybe it was in doctrine.yaml :
doctrine:
orm:
auto_generate_proxy_classes: true
naming_strategy: doctrine.orm.naming_strategy.underscore
auto_mapping: true
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
But I have not found any option to allow the number aware :(
In most cases I would just answer this sort of question with a comment but I suspect other developers might run into this issue. I poked around a bit and could not find any explicit documentation on this issue. Perhaps because the DoctrineBundle is under the control of the Doctrine folks and not the Symfony developers. Or maybe I am just a bad searcher.
In any event, between 4.3 and 4.4 the service name for the underscore naming strategy was changed.
# doctrine.yaml
orm:
# 4.3
naming_strategy: doctrine.orm.naming_strategy.underscore
# 4.4
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
And a deprecated message was added to warn developers to change the name. Would have been nice if the message was just a tiny bit more explicit but oh well.
So if you are upgrading an existing app to 4.4 and beyond then you will probably need to manually edit your doctrine.yaml file to make the depreciation message go away.
Some more info (thanks #janh) on why the change was made:
https://github.com/doctrine/orm/blob/2.8.x/UPGRADE.md#deprecated-number-unaware-doctrineormmappingunderscorenamingstrategy
https://github.com/doctrine/orm/issues/7855
Still not really clear on why "they" chose to do things this way but oh well.
You probably want to run "bin/console doctrine:schema:update --dump-sql" just to see if this impacts your database column names and adjust accordingly. The changes has been out for several weeks now and there does not seem to be many howls of outrage over the change so I guess most column names don't have embedded numbers. So far at least.
For those who works with symfony4.3 and still want this warning disapear you can add add new new service defintion in service.yaml
custom_doctrine_orm_naming_strategy_underscore:
class: Doctrine\ORM\Mapping\UnderscoreNamingStrategy
arguments:
- 0
- true
and change the configuration of doctrine.yaml like this:
orm:
naming_strategy: custom_doctrine_orm_naming_strategy_underscore
before going straight forward committing this change I would suggest you to verify that passing true to the Doctrine\ORM\Mapping\UnderscoreNamingStrategy doesn't affect the expected behavior of your code.
// class UnderscoreNamingStrategy
/**
* Underscore naming strategy construct.
*
* #param int $case CASE_LOWER | CASE_UPPER
*/
public function __construct($case = CASE_LOWER, bool $numberAware = false)
{
if (! $numberAware) {
#trigger_error(
'Creating ' . self::class . ' without making it number aware is deprecated and will be removed in Doctrine ORM 3.0.',
E_USER_DEPRECATED
);
}
$this->case = $case;
$this->pattern = $numberAware ? self::NUMBER_AWARE_PATTERN : self::DEFAULT_PATTERN;
}
Quick hint:
passing true to the c'tor will make the class use the NUMBER_AWARE_PATTERN instead of the DEFAULT_PATTERN
private const DEFAULT_PATTERN = '/(?<=[a-z])([A-Z])/';
private const NUMBER_AWARE_PATTERN = '/(?<=[a-z0-9])([A-Z])/';

Symfony2 jms/i18n-routing-bundle and multiple hosts to one locale

I am using mentioned bundle in my application, and I would like to be able to configure it this way:
jms_i18n_routing:
default_locale: en
locales: [en, de]
strategy: custom
hosts:
en: [mydomain.com, subdomain.domain.com]
de: mydomain.de
redirect_to_host: false
so multiple domains to one locale. I would like to run two similiar websites at one application to have access to the 90% of the code which is similiar and same database. Any tips how could i achieve this? Or maybe there is other bundle/solution more accurate for my problem?
From the configuration you cannot bind multiple domains to one locale.
You can try to extend this class of the bundle:
JMS\I18nRoutingBundle\Router\DefaultLocaleResolver
You need to change this part:
public function resolveLocale(Request $request, array $availableLocales)
{
if ($this->hostMap && isset($this->hostMap[$host = $request->getHost()])) {
return $this->hostMap[$host];
}
...
}
adding a more complex hostMap that supports multiple domains for the same locale.

Login in symfony2

I'm trying to implement very basic authentication in Symfony2. Here are main parts of the code I really don't see any problem
EDIT
complete security.yml
jms_security_extra:
secure_all_services: false
expressions: true
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
in_memory:
memory:
users:
user: { password: userpass, roles: [ 'ROLE_USER' ] }
admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
firewalls:
login:
pattern: ^/login
anonymous: ~
secured_area:
pattern: ^/
stateless: true
form_login:
login_path: /login
check_path: /login_check
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
This works fine, anonymous user is always redirected to loginAction controller.
EDIT
Here is the complete code
<?php
namespace AcmeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
class SecurityController extends Controller {
public function loginAction() {
$providerKey = 'secured_area';
$token = new UsernamePasswordToken('test', 'test', $providerKey, array('ROLE_USER'));
$this->container->get('security.context')->setToken($token);
return $this->redirect($this->generateUrl('fronthomepage'));
}
}
I don't see any problem, anonymous user is redirected to loginAction, there is created authenticated user, saved to token and than redirected to secured area as an authenticated user. Unfortunately my code ends with redirect loop which looks like security firewall doesn't accept user as authenticated. Do you see any problem?
Well, your controller job is to render just form but not to populate security context. Symfony2 security firewall will do that for you automatically. You don't need to handle it unless you want to build you own custom authentication.
In other words, your job is to display the login form and any login
errors that may have occurred, but the security system itself takes
care of checking the submitted username and password and
authenticating the user.
Please read this document for clear picture.
If you want to do some custom stuff when a user logs in, in Symfony2 you have to add an event listener that will fire after the user successfully logged in. The event that is fired is security.interactive_login and to hook to it you have to specify this in services.yml file form your bundle Resources/config directory:
Pretty sure you need an actual user object before setting an authenticated user. I did something like this:
class BaseController
protected function setUser($userName)
{
if (is_object($userName)) $user = $userName;
else
{
$userProvider = $this->get('zayso_core.user.provider');
// Need try/catch here
$user = $userProvider->loadUserByUsername($userName);
}
$providerKey = 'secured_area';
$providerKey = $this->container->getParameter('zayso_core.provider.key'); // secured_area
$token = new UsernamePasswordToken($user, null, $providerKey, $user->getRoles());
$this->get('security.context')->setToken($token);
return $user;
}
However doing something like this bypasses much of the security system and is not recommended. I also wanted to use a 3rd party authentication system (Janrain). I looked at the authentication system and initially could not make heads or tails out of it. This was before the cookbook entry existed.
I know it seems overkill but once you work through things then it starts to make more sense. And you get access to a bunch of nifty security functions. It took me quite some time to start to understand the authentication system but it was worth it in the end.
Hints:
1. Work through the cook book backward. I had a real hard time understanding what was going on but I started with adding a new firewall to security.yml and then adding the alias for my security factory. I then sort of traced through what the factory was being asked to do. From there I got the listener to fire up and again traced through the calls. Finally the authentication manager comes into play. Again, time consuming, but worth it in the end. Learned a lot.
One thing that drove me crazy is that classes are scattered all over the place. And the naming leaves something to be desired. Very hard to get an overview. I ended up making my own authentication bundle then putting everything under security.
If you want another example of a working bundle then take a look at: https://github.com/cerad/cerad/tree/master/src/Cerad/Bundle/JanrainBundle

Resources