How to impersonate user by id instead of username in symfony? - symfony

I can't figure out how to impersonate a user by user's id instead of user's username in Symfony?
The following trick which works with username can't work with id, as symfony is looking for username:
?_switch_user={id}

This is impossible to do without implementing your own firewall listener, as behind the scenes it loads the user from the userprovider (which only has a loadUserByUsername() method in its interface).
You could however implement your own firewall listener and get inspired by having a look at the code in Symfony\Component\Security\Http\Firewall\SwitchUserListener. For detailed information on implementing your own authentication provider, check the cookbook article.
EDIT:
One possible solution might be registering an extra request listener:
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LookupSwitchUserListener implements EventSubscriberInterface
{
private $repository;
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['lookup', 12] // before the firewall
];
}
public function lookup(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($request->has('_switch_user') {
return; // do nothing if already a _switch_user param present
}
if (!$id = $request->query->has('_switch_user_by_id')) {
return; // do nothing if no _switch_user_by_id param
}
// lookup $username by $id using the repository here
$request->attributes->set('_switch_user', $username);
}
}
Now register this listener in the service container:
services:
my_listener:
class: LookupSwitchUserListener
tags:
- { name: kernel.event_subscriber }
Calling a url with the ?_switch_user_by_id=xxx parameter should now correctly look up the username and set it so the SwitchUserListener can switch to the specified user.

Related

How to get dynamic security privileges with Symfony Security

I'm need to do add custom access law to my access control on Symfony, I try to explain me.
I have a web application with some customer, and I want to able the access of some part of the code when that customer have the right plugin. So this is my customer:
namespace AppBundle\Entity;
class CustomerProfile{
private $id;
private $user;
private $plugins;
}
The Entity for the Plugin
namespace AppBundle\Entity;
class Plugin{
private $id;
private $name;
private $customerProfiles;
}
I use doctrine for the relations so from customer i can get his plugins. For example, we have 2 customer and 2 plugin:
AppBundle\Entity\CustomerProfile:
customer_1:
user: '#user_1'
plugins: ['#plugin_1','#plugin_2']
customer_2:
user: '#user_2'
plugins: ['#plugin_1']
AppBundle\Entity\Plugin:
plugin_1:
name: 'plugin 1'
plugin_2:
name: 'plugin 2'
In my project all the code about customer is under the /customer namespace, Symfony like, and all work.
access_control:
- { path: ^/customer, roles: ROLE_CUSTOMER }
But, for this customer with different plugin i would set an dinamic access control, but i don't know how. I need to control something like this:
access_control:
- { path: ^/code_for_plugin_1, roles: ROLE_CUSTOMER_WHIT_PLUGIN_1}
- { path: ^/code_for_plugin_2, roles: ROLE_CUSTOMER_WHIT_PLUGIN_2}
but I think the good way is set a "sub role" (if exist) to set for each customer that have a plugin a role to access in that namespace.
I hope I was clear enough, thanks for help.
I would suggest to use custom voter rather than the access_control and role approach as
this is in my opinion more flexible for your use case. Your suggested solution requires
a generated role (ROLE_CUSTOMER_WHIT_PLUGIN_{X}) for every plugin, which in case
of adding pluggins dynamically would just not work.
Check the How to Use Voters to Check User Permissions article in Symfony documentation for more detail.
You basically need to implement a user voter which will check if the logged user has an access
to requested resource. In your case it would look similar to this:
/src/YourBundle/Controller/YourController.php
<?php
...
class YourController extends Controller
{
public function getFooAction($id)
{
$this->denyAccessUnlessGranted(YourVoter::VIEW_FOO);
// ...method logic
}
public function getBarAction($id)
{
$this->denyAccessUnlessGranted(YourVoter::VIEW_BAR);
// ...method logic
}
}
/src/YourBundle/Security/YourVoter.php
<?php
...
class YourVoter extends AbstractVoter
{
const VIEW_FOO = 'YOUR_VIEW_FOO';
const VIEW_BAR = 'YOUR_VIEW_BAR';
public function getVoterAttributes()
{
return [self::VIEW_FOO, self::VIEW_BAR,];
}
protected function supports($attribute, $subject)
{
...
}
protected function voteOnAttribute($attribute, $item, TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
switch ($attribute) {
case self::VIEW_FOO:
return $this->canViewFoo($user);
case self::VIEW_BAR:
return $this->canViewBar($user);
}
throw new \Exception(sprintf(
'Invalid vote attribute "%s".',
$attribute
));
}
private function canViewFoo(User $user)
{
return $user->getProfile()->hasRoleFooXYZ()
}
private function canViewBar(User $user)
{
return $user->getProfile()->hasRoleBarXYZ()
}
}

Sysmfony REST API hash id of entities

I'm building a multitenancy backend using Symfony 2.7.9 with FOSRestBundle and JMSSerializerBundle.
When returning objects over the API, I'd like to hash all the id's of the returned objects, so instead of returning { id: 5 } it should become something like { id: 6uPQF1bVzPA } so I can work with the hashed id's in the frontend (maybe by using http://hashids.org)
I was thinking about configuring JMSSerializer to set a virtual property (e.g. '_id') on my entities with a custom getter-method that calculates the hash for the id, but I don't have access to the container / to any service.
How could I properly handle this?
You could use a Doctrine postLoad listener to generate a hash and set a hashId property in your class. Then you could call expose the property in the serializer but set the serialized_name as id (or you could just leave it at hash_id).
Due to the hashing taking place int the postLoad you would need to refresh your object if you have just created it using $manager->refresh($entity) for it take effect.
AppBundle\Doctrine\Listener\HashIdListener
class HashIdListsner
{
private $hashIdService;
public function postLoad(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
$reflectionClass = new \ReflectionClass($entity);
// Only hash the id if the class has a "hashId" property
if (!$reflectionClass->hasProperty('hashId')) {
return;
}
// Hash the id
$hashId = $this->hashIdService->encode($entity->getId());
// Set the property through reflection so no need for a setter
// that could be used incorrectly in future
$property = $reflectionClass->getProperty('hashId');
$property->setAccessible(true);
$property->setValue($entity, $hashId);
}
}
services.yml
services:
app.doctrine_listsner.hash_id:
class: AppBundle\Doctrine\Listener\HashIdListener
arguments:
# assuming your are using cayetanosoriano/hashids-bundle
- "#hashids"
tags:
- { name: doctrine.event_listener, event: postLoad }
AppBundle\Resources\config\serializer\Entity.User.yml
AppBundle\Entity\User:
exclusion_policy: ALL
properties:
# ...
hashId:
expose: true
serialized_name: id
# ...
Thanks a lot for your detailed answer qooplmao.
However, I don't particularly like this approach because I don't intend to store the hashed in the entity. I now ended up subscribing to the serializer's onPostSerialize event in which I can add the hashed id as follows:
use JMS\Serializer\EventDispatcher\EventSubscriberInterface;
use JMS\Serializer\EventDispatcher\ObjectEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MySubscriber implements EventSubscriberInterface
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public static function getSubscribedEvents()
{
return array(
array('event' => 'serializer.post_serialize', 'method' => 'onPostSerialize'),
);
}
/**
* #param ObjectEvent $event
*/
public function onPostSerialize(ObjectEvent $event)
{
$service = $this->container->get('myservice');
$event->getVisitor()->addData('_id', $service->hash($event->getObject()->getId()));
}
}

How do I check the User's IP in a Request subscriber/listener in Symfony2?

Some users of my application will have an allowedIPs array attached. There was a guide for an authentication Voter for blacklisting IPs which I could adapt to whitelist a user's IP based on who is authenticating.
The problem I see here is a scenario where a user authenticates while in one allowed network, then switches to another network where the user is not allowed to connect from.
I think the solution would be to have a subscriber to the kernel.request event, where I deauthorize the user if the IP is not allowed.
Is this sort of IP checking on each request a stupid approach? If it's not, how do I get the authenticated user in the event subscriber? The GetResponseEvent (api docs) doesn't seem to give any method from which to get the authenticated user if one exists.
EDIT: As Cerad suggested, I did this with a voter.
Voter class
<?php
namespace My\UserBundle\Security;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Psr\Log\LoggerInterface;
use My\UserBundle\Entity\User;
class ValidClientIpVoter extends AuthenticatedVoter
{
private $container;
private $logger;
public function __construct(ContainerInterface $container, LoggerInterface $logger)
{
$this->container = $container;
$this->logger = $logger;
}
public function vote(TokenInterface $token, $object, array $attributes)
{
$request = $this->container->get('request');
$user = $token->getUser();
// vote on instances of our User class
if($user instanceof User) {
$allowed_ips = $user->getAllowedIps();
// only vote if there actually are limitations
if(is_array($allowed_ips) && count($allowed_ips)) {
$this->logger->debug(sprintf('ValidClientIpVoter: Validating allowed IPs for user #%d', $user->getId()));
// deny access if current request's IP is not allowed for the user
if(!in_array($request->getClientIp(), $allowed_ips)) {
$this->logger->notice(sprintf('ValidClientIpVoter: Invalid client IP for user #%d', $user->getId()));
return VoterInterface::ACCESS_DENIED;
}
}
}
return VoterInterface::ACCESS_ABSTAIN;
}
}
changes to security.yml to make voting unanimous
security:
access_decision_manager:
strategy: unanimous
and finally the service definition
services:
valid_client_ip_voter:
class: My\UserBundle\Security\ValidClientIpVoter
arguments: [#service_container, #monolog.logger]
public: false
tags:
- { name: security.voter }
To get the current user, inject the security.token_storage service into your listener: http://symfony.com/doc/current/book/security.html#retrieving-the-user-object
I would actually listen to kernel.controller just to make sure the user is available.
On the other hand, voters get called on each access to a resource so I am unclear why your existing approach would not work even if the ip changes.

Set the email manually with FOSUserBundle

I am using Symfony 2 + FOSUserBundle to manage my users.
The problem is that I would like to build the email address "manually" from the username, and so, do not require any field for the email.
I will create the email from the username because one requirement to sign up is to have an email from my university which follows a strict format (username#schoolname.fr).
I managed to override the RegistrationFormType to avoid the email field from being added to my sign up page but I still have the error "Please enter an email" when I submit the form.
How can I prevent the validation of the email address and how could I "build" it from the username?
Thanks!
(Sorry for the English, I know it's not perfect...)
There is a simple way around it. It is even mentioned in the official FOSUserBundle documentation. You just need to override the controller.
Create your own Bundle and extend the FOSUserBundle:
class CustomUserBundle extends Bundle
{
public function getParent()
{
return 'FOSUserBundle';
}
}
And then override the RegistrationController:
class RegistrationController extends BaseController
{
public function registerAction(Request $request)
{
// here you can implement your own logic. something like this:
$user = new User();
$form = $this->container->get('form.factory')->create(new RegistrationType(), $user);
if ($request->getMethod() == 'POST') {
$form->bind($request);
if ($form->isValid()) {
$user->setEmail($user->getUsername() . '#' . $user->getSchoolname() . '.fr');
// and what not. Also don't forget to either activate the user or send an activation email
}
}
}
}
You should write event listener for fos_user.registration.initialize. From code docs:
/**
* The REGISTRATION_INITIALIZE event occurs when the registration process is initialized.
*
* This event allows you to modify the default values of the user before binding the form.
* The event listener method receives a FOS\UserBundle\Event\UserEvent instance.
*/
const REGISTRATION_INITIALIZE = 'fos_user.registration.initialize';
More info about event dispatcher: http://symfony.com/doc/current/components/event_dispatcher/introduction.html
And example event listener: http://symfony.com/doc/current/cookbook/service_container/event_listener.html
UPDATE - how to code?
In your config.yml (or services.yml or other extension like xml, php) define service like this:
demo_bundle.listener.user_registration:
class: Acme\DemoBundle\EventListener\Registration
tags:
- { name: kernel.event_listener, event: fos_user.registration.initialize, method: overrideUserEmail }
Next, define listener class:
namespace Acme\DemoBundle\EventListener;
class Registration
{
protected function overrideUserEmail(UserEvent $args)
{
$request = $args->getRequest();
$formFields = $request->get('fos_user_registration_form');
// here you can define specific email, ex:
$email = $formFields['username'] . '#sth.com';
$formFields['email'] = $email;
$request->request->set('fos_user_registration_form', $formFields);
}
}
Notice: Of course you can validate this email via injecting #validator to the listener.
Now you should hide email field in registration form. YOu can do that by overriden register_content.html.twig or (in my oppinion better way) override FOS RegistrationFormType like this:
namespace Acme\DemoBundle\Form\Type;
use FOS\UserBundle\Form\Type\RegistrationFormType as BaseType;
use Symfony\Component\Form\FormBuilderInterface;
class RegistrationFormType extends BaseType
{
// some code like __construct(), getName() etc.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// some code for your form builder
->add('email', 'hidden', array('label' => 'form.email', 'translation_domain' => 'FOSUserBundle'))
;
}
}
Now your application is ready for setting email manually.

Dynamically adding roles to a user

We are using Symfony2's roles feature to restrict users' access to certain parts of our app. Users can purchase yearly subscriptions and each of our User entities has many Subscription entities that have a start date and an end.
Now, is there a way to dynamically add a role to a user based on whether they have an 'active' subscription? In rails i would simply let the model handle whether it has the necessary rights but I know that by design symfony2 entities are not supposed to have access to Doctrine.
I know that you can access an entity's associations from within an entity instance but that would go through all the user's subscription objects and that seems unnecessaryly cumbersome to me.
I think you would do better setting up a custom voter and attribute.
/**
* #Route("/whatever/")
* #Template
* #Secure("SUBSCRIPTION_X")
*/
public function viewAction()
{
// etc...
}
The SUBSCRIPTION_X role (aka attribute) would need to be handled by a custom voter class.
class SubscriptionVoter implements VoterInterface
{
private $em;
public function __construct($em)
{
$this->em = $em;
}
public function supportsAttribute($attribute)
{
return 0 === strpos($attribute, 'SUBSCRIPTION_');
}
public function supportsClass($class)
{
return true;
}
public function vote(TokenInterface $token, $object, array $attributes)
{
// run your query and return either...
// * VoterInterface::ACCESS_GRANTED
// * VoterInterface::ACCESS_ABSTAIN
// * VoterInterface::ACCESS_DENIED
}
}
You would need to configure and tag your voter:
services:
subscription_voter:
class: SubscriptionVoter
public: false
arguments: [ #doctrine.orm.entity_manager ]
tags:
- { name: security.voter }
Assuming that you have the right relation "subscriptions" in your User Entity.
You can maybe try something like :
public function getRoles()
{
$todayDate = new DateTime();
$activesSubscriptions = $this->subscriptions->filter(function($entity) use ($todayDate) {
return (($todayDate >= $entity->dateBegin()) && ($todayDate < $entity->dateEnd()));
});
if (!isEmpty($activesSubscriptions)) {
return array('ROLE_OK');
}
return array('ROLE_KO');
}
Changing role can be done with :
$sc = $this->get('security.context')
$user = $sc->getToken()->getUser();
$user->setRole('ROLE_NEW');
// Assuming that "main" is your firewall name :
$token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken($user, null, 'main', $user->getRoles());
$sc->setToken($token);
But after a page change, the refreshUser function of the provider is called and sometimes, as this is the case with EntityUserProvider, the role is overwrite by a query.
You need a custom provider to avoid this.

Resources