Registro con ventana modal con FOSUserBundle en Symfony 3

Comumente las aplicaciones web desarrolladas ya sea con Symfony o no, suelen tener ciertas caracteristicas similares(no en todos los casos, obviamente), pero muchas de ellas se parecen en algunas funcionalidades. En este caso, y como este blog trata sobre Symfony, voy a explicar un caso comun sobre la gestion de usuarios, y mas concretamente el registro de usuario, y su solucion usando el FOSUserBundle para la gestion de usuarios en Symfony 3.

Generalmente las aplicaciones web, pueden ofrecer al usuario una de estas 2 formas para que se registren en el web.

Una de esas opciones y la mas “sencilla”, seria la de una pagina o vista con un formulario donde el usuario rellena dichos campos, y al enviar dicho formulario, se le envia a otra pagina distinta(ya sea de bienvenida, de procesar compra, etc.). Digo sencilla porque el FOSUserBundle de Symfony 3 ya ofrece esta funcionalidad de esa forma.

La segunda forma, seria la que podriamos ver cuando pulsamos un boton de la cabecera o header, y nos aparece una ventana modal en forma de capa con un fondo oscuro, en la que nos aparece el formulario con los campos, y que nos guia a traves de ese modal a otra parte de la aplicacion. (He tenido que ocultar ciertos detalles de esta captura por temas legales y de privacidad)

Registro con ventana modal
Registro con ventana modal

A efectos logicos, podria ser practicamente igual una forma que la otra, pero a efectos practicos no es igual, y su solucion es algo mas compleja que reescribir las vistas del FOSUserBundle y alguna que otra cosa mas. Tenemos que hacer varias cosas mas para que todo esto funcione y sea correcto usando las buenas practicas en Symfony 3. Dicho esto, comenzamos.

NOTA: en el proyecto estoy usando Symfony 3 y la ultima version del FOSUSerBundle, aunque supongo que para Symfony 2 sera practicamente igual.

1- Instalamos FOSUserBundle(composer.json)

“friendsofsymfony/user-bundle”: “dev-master”

2- Inicializamos en bundle en la aplicacion(app/AppKernel.php):

new FOS\UserBundle\FOSUserBundle(),

3- Importamos las rutas del FOSUserBundle para que esten accesibles:

#fos_user:
#    resource: "@FOSUserBundle/Resources/config/routing/all.xml"

fos_user_security:
    resource: "@FOSUserBundle/Resources/config/routing/security.xml"

fos_user_profile:
    resource: "@FOSUserBundle/Resources/config/routing/profile.xml"
    prefix: /profile

fos_user_register:
    resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
    prefix: /register

fos_user_resetting:
    resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
    prefix: /resetting

fos_user_change_password:
    resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
    prefix: /profile

NOTA: Atentos a que la primera ruta esta comentada, pues importa todas las rutas del FOS, pero puede que no las queramos tener todas disponibles, por eso las importamos una a una a mano, mas abajo.

4- Configuramos el bundle(app/config/config.yml)

Una buena practica es crear un nuevo archivo por ejemplo “fos.yml” y añadir la configuracion especifica ahi, y despues importarlo en el config.yml.

fos_user:
    db_driver: orm # other valid values are 'mongodb', 'couchdb' and 'propel'
    firewall_name: main
    user_class: UserBundle\Entity\User
    registration:
        form:
            type: UserBundle\Form\RegistrationType
    use_username_form_type: false

NOTA: Por una parte, configuramos para que el formulario de registro use nuestro formulario(ya que lo tendremos que sobrescribir) y despues desactivamos que el formulario haga uso de la validacion por el campo username. Mas adelante, lo explicare.

5- Definimos la seguridad, roles, y providers(app/config/security.yml)

# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:

    encoders:
        UserBundle\Entity\User: sha512
        FOS\UserBundle\Model\UserInterface: sha512

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: ROLE_ADMIN

    # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
    providers:
        fos_userbundle:
            id: fos_user.user_provider.username_email

    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            pattern: ^/
            form_login:
                provider: fos_userbundle
                csrf_token_generator: security.csrf.token_manager
                login_path: fos_user_security_login
                check_path: fos_user_security_check
                failure_path: fos_user_security_login
                always_use_default_target_path: true
                default_target_path: homepage
                # if you are using Symfony < 2.8, use the following config instead:
                # csrf_provider: form.csrf_provider

            logout:       true
            anonymous:    true
            # activate different ways to authenticate

            # http_basic: ~
            # http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate

            # form_login: ~
            # http://symfony.com/doc/current/cookbook/security/form_login_setup.html

    access_control:
        - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin/, role: ROLE_ADMIN }

NOTA: En nuestro caso, tenemos un par de roles muy sencillos y poco mas.

6- Creamos la entity User que extenderá la que lleva el FOSUserBundle, con algunos campos más:

use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="ecommerce_user")
 */
class User extends BaseUser
{
 /**
 * @ORM\Id
 * @ORM\Column(type="integer")
 * @ORM\GeneratedValue(strategy="AUTO")
 */
 protected $id;

 /**
 * @ORM\Column(type="string", length=250, nullable=true)
 */
 protected $name;

 /**
 * @ORM\Column(type="string", length=250, nullable=true)
 */
 protected $surname;

 public function __construct()
 {
     parent::__construct();
     // your own logic

     $this->enabled = true;
 }

 /**
 * @return string
 */
 public function getName()
 {
 return $this->name;
 }

 /**
 * @param string $name
 */
 public function setName($name)
 {
 $this->name = $name;
 }

 /**
 * @return string
 */
 public function getSurname()
 {
 return $this->surname;
 }

 /**
 * @param string $surname
 */
 public function setSurname($surname)
 {
 $this->surname = $surname;
 }

 /**
 * @param string $email
 *
 * @return $this|\FOS\UserBundle\Model\UserInterface
 */
 public function setEmail($email)
 {
     $this->setUsername($email);
     return parent::setEmail($email);
 }
}

NOTA: Prestad especial atencion al metodo setEmail(). Este metodo esta implementado en la clase BaseUser del FOS, pero nosotros necesitamos “sobrescribirlo” porque en nuestro caso el email sera nuestro “Identificador” del usuario con el que se podra “logear” en la aplicacion web.

Por supuesto, hay que aplicar los nuevas cambios en la base de datos usando el comando:

$ php bin/console doctrine:schema:update –force –dump-sql

7- Sobrescribir el formulario de registro del FOS por el nuestro personalizado:

class RegistrationType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class, array(
                'label' => 'Nombre',
                'required' => true,
                'constraints' => array(
                    new NotBlank(),
                ),
            ))
            ->add('surname', TextType::class, array(
                'label' => 'Apellidos',
                'required' => true,
                'constraints' => array(
                  new NotBlank(),
                ),
            ))
            ->add('email', EmailType::class, array(
                'label' => 'Email',
                'required' => true,
                'constraints' => array(
                    new NotBlank(),
                ),
            ))
            ->add('plainPassword', TextType::class, array(
                'label' => 'Contraseña',
                'required' => true,
                'constraints' => array(
                    new NotBlank(),
                )
            )
        );
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'UserBundle\Entity\User',
            'csrf_token_id' => 'registration'
        ));
    }

    public function getBlockPrefix()
    {
        return 'user_registration';
    }
}

En  nuestro caso, no necesitamos el campo “username” o “password” con validacion que “impone” el formulario base del FOSUserBundle. Es por ello, que lo sobrescribimos de esta forma. Si nos acordamos bien, definimos en fos.yml que el formulario de registro iba a ser uno nuestro. Pues bien, este es el que se usara.

Uno de los problemas que me encontre al usar el formulario base del FOS fue que esos campos(username, o password con validacion) debian mostrarse si o si, y aunque se oculten para no mostrarse, el formulario daba error, sobretodo por Javascript al decir que dichos campos no eran “focusable” , es decir, no podia situarse el foco(raton, tabulador teclado, etc.) en ellos.

8- Sobrescribir el controlador del registro

Aqui os explicare porque, ya que en mi caso, el controller base del FOS redirige a una pagina nueva, y en nuestro caso no es asi, sino la misma pagina donde estabamos pero mostrando la ventana modal. Por lo tanto, necesitamos sobrescribir dicha funcionalidad.

El bundle nos dice en su documentacion, que para poder “sobrescribir” un Controller, necesitamos crear un bundle “hijo” que herede del FOS.

8.1- Creamos un nuevo UserBundle

$ php bin/console generate:bundle –namespace=UserBundle –dir=src

8.2- Le decimos que nuestro nuevo bundle herede del FOSUserBundle (src/UserBundle/UserBundle.php)

class UserBundle extends Bundle
{
    public function getParent()
    {
        return 'FOSUserBundle';
    }
}

8.3- Creamos un nuevo RegisterController.php que sustituira el del FOS

<?php

/*
 * This file overrides the FOSUserBundle registration controller.
 *
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace UserBundle\Controller;

use AppBundle\Controller\BaseController;
use FOS\UserBundle\Event\FilterUserResponseEvent;
use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\Event\GetResponseUserEvent;
use FOS\UserBundle\Form\Factory\FactoryInterface;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Model\UserInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

/**
 * Controller managing the registration.
 *
 * @author Thibault Duplessis <thibault.duplessis@gmail.com>
 * @author Christophe Coevoet <stof@notk.org>
 */
class RegistrationController extends BaseController
{
    /**
     * @param Request $request
     *
     * @return Response
     */
    public function registerAction(Request $request)
    {
        /** @var $formFactory FactoryInterface */
        $formFactory = $this->get('fos_user.registration.form.factory');
        /** @var $userManager UserManagerInterface */
        $userManager = $this->get('fos_user.user_manager');
        /** @var $dispatcher EventDispatcherInterface */
        $dispatcher = $this->get('event_dispatcher');

        $user = $userManager->createUser();

        $event = new GetResponseUserEvent($user, $request);
        $dispatcher->dispatch(FOSUserEvents::REGISTRATION_INITIALIZE, $event);

        if (null !== $event->getResponse()) {
            return $event->getResponse();
        }

        $form = $formFactory->createForm();
        $form->setData($user);

        $form->handleRequest($request);

        if ($form->isSubmitted()) {
            if ($form->isValid()) {
                $event = new FormEvent($form, $request);
                $dispatcher->dispatch(FOSUserEvents::REGISTRATION_SUCCESS, $event);

                $userManager->updateUser($user);

                if (null === $response = $event->getResponse()) {
                    $url = $this->generateUrl('fos_user_registration_confirmed');
                    $response = new RedirectResponse($url);
                }

                $dispatcher->dispatch(FOSUserEvents::REGISTRATION_COMPLETED, new FilterUserResponseEvent($user, $request, $response));

                return $response;
            }

            $event = new FormEvent($form, $request);
            $dispatcher->dispatch(FOSUserEvents::REGISTRATION_FAILURE, $event);

            if (null !== $response = $event->getResponse()) {
                return $response;
            }
        }

        return $this->redirectToReferer($request);

//        return $this->render('FOSUserBundle:Registration:register.html.twig', array(
//            'form' => $form->createView(),
//        ));
    }

    /**
     * Tell the user to check his email provider.
     */
    public function checkEmailAction()
    {
        $email = $this->get('session')->get('fos_user_send_confirmation_email/email');

        if (empty($email)) {
            return new RedirectResponse($this->get('router')->generate('fos_user_registration_register'));
        }

        $this->get('session')->remove('fos_user_send_confirmation_email/email');
        $user = $this->get('fos_user.user_manager')->findUserByEmail($email);

        if (null === $user) {
            throw new NotFoundHttpException(sprintf('The user with email "%s" does not exist', $email));
        }

        return $this->render('FOSUserBundle:Registration:check_email.html.twig', array(
            'user' => $user,
        ));
    }

    /**
     * Receive the confirmation token from user email provider, login the user.
     *
     * @param Request $request
     * @param string  $token
     *
     * @return Response
     */
    public function confirmAction(Request $request, $token)
    {
        /** @var $userManager \FOS\UserBundle\Model\UserManagerInterface */
        $userManager = $this->get('fos_user.user_manager');

        $user = $userManager->findUserByConfirmationToken($token);

        if (null === $user) {
            throw new NotFoundHttpException(sprintf('The user with confirmation token "%s" does not exist', $token));
        }

        /** @var $dispatcher EventDispatcherInterface */
        $dispatcher = $this->get('event_dispatcher');

        $user->setConfirmationToken(null);
        $user->setEnabled(true);

        $event = new GetResponseUserEvent($user, $request);
        $dispatcher->dispatch(FOSUserEvents::REGISTRATION_CONFIRM, $event);

        $userManager->updateUser($user);

        if (null === $response = $event->getResponse()) {
            $url = $this->generateUrl('fos_user_registration_confirmed');
            $response = new RedirectResponse($url);
        }

        $dispatcher->dispatch(FOSUserEvents::REGISTRATION_CONFIRMED, new FilterUserResponseEvent($user, $request, $response));

        return $response;
    }

    /**
     * Tell the user his account is now confirmed.
     */
    public function confirmedAction()
    {
        $user = $this->getUser();
        if (!is_object($user) || !$user instanceof UserInterface) {
            throw new AccessDeniedException('This user does not have access to this section.');
        }

        return $this->render('FOSUserBundle:Registration:confirmed.html.twig', array(
            'user' => $user,
            'targetUrl' => $this->getTargetUrlFromSession(),
        ));
    }

    /**
     * @return mixed
     */
    private function getTargetUrlFromSession()
    {
        $key = sprintf('_security.%s.target_path', $this->get('security.token_storage')->getToken()->getProviderKey());

        if ($this->get('session')->has($key)) {
            return $this->get('session')->get($key);
        }
    }
}

Si observais en rojo he comentado el render de su propia vista, y en verde mi codigo, que devuelve al usuario a la pagina de donde a venido. Podriamos pasarle algun parametro o flag para que cuando se cargue esa vista, abra automaticamente la ventana modal y muestre algo, por ejemplo. Eso lo dejo a vuestro a vuestra eleccion. Ademas, tambien tenemos que implementar el resto de metodos que hay en dicho controlador, en mi caso los he dejado igual que estaban.

SUGERENCIA: Al comenzar el proyecto solo tenia un unico bundle (AppBundle) donde iba metiendo la logica de negocio de la aplicacion web. Al necesitar “sobrescribir” el FOSUserBundle, he necesitado crear un nuevo UserBundle por lo que “todo” lo relacionado con los usuarios lo he migrado al UserBundle(Controllers, vistas, js, Entity, etc.) para no mezclarlo con el resto de cosas que iran en ApBundle.

El resto ya es cosa vuestra, es decir debereis usar algun plugin de jQuery o lo que useis para implementar vuestro “overlay” o dialogo modal y pintar alli el formulario. Os dejo un ejemplo de como quedaria el mio:

REGISTER-MODAL.HTML.TWIG

El action del controller quedaria de la siguiente forma:

/**
 * @Route("/", name="homepage")
 */
public function indexAction(Request $request)
{
    /** @var $formFactory FactoryInterface */
    $formFactory = $this->get('fos_user.registration.form.factory');
    $user = new User();
    $form = $formFactory->createForm();
    $form->setData($user);

    return $this->render('AppBundle::homepage.html.twig', array(
        'form' => $form->createView()
    ));
}

El siguiente problema al que me enfrento es el siguiente: Imaginad que mi aplicacion web tiene 10 paginas en el front. En la cabecera(header) es donde se llama a la ventana modal para mostrar el formulario de registro, es decir, que por logica en cada Action del Controller deberia crear el formulario cada vez y enviarselo como parametro a cada vista( 10 veces al menos ).

Para evitar duplicar el mismo bloque cada vez, me ha pasado un buen amigo una idea que me parece bastante acertada, y consiste en usar la funcion “render” de twig para llamar a un controller y que este se encargue de crear el formulario. Esto se haria asi:

En nuestro template(footer.html.twig) usamos la funcion twig asi:

{{ render(controller('AppBundle:Default:registerModal')) }}

Y en nuestro controller, creamos el formulario de registro y pintamos la vista “parcial”, y hacemos lo siguiente:

/**
 * @Route("/register-modal", name="register_modal")
 */
public function registerModalAction()
{
    /** @var $formFactory FactoryInterface */
    $formFactory = $this->get('fos_user.registration.form.factory');
    $user = new User();
    $form = $formFactory->createForm();
    $form->setData($user);

    return $this->render('UserBundle:user:register-modal.html.twig', array(
        'form' => $form->createView()
    ));
}

De esta forma, todas las vistas que muestren el footer, construiran el formulario de registro y mostraran la ventana modal con el registro de usuario, asi no “duplicamos” la misma logica en todos los action del controlador.

 

Espero que os haya gustado, y si teneis otra forma distinta de hacerlo o teneis alguna pregunta, no dudeis en preguntar y compartirlo en las redes sociales.

Saludos SymfonyDevs ¡¡

Anuncios

Uso de Voters para el control del acceso de usuarios en zonas

Acceso denegado - 403
Acceso denegado – 403

Los voters son un elemento de Symfony para darle una vuelta de tuerca al las listas de acceso – ACL (Access Control List) de Symfony donde podemos validar con mas profundidad si un recurso esta disponible para un usuario.

Aqui la documentacion oficial.

A continuacion explicare un ejemplo mas concreto donde lo he usado.

La problematica era la siguiente:

-Un usuario puede realizar ciertas acciones en las zonas, segun el tipo de rol y los permisos definidos para cada rol y zona. Es decir que para mi zona de “Gestion de Usuarios” existen las opciones de listar, crear, editar, visualizar y eliminar usuarios. Por ejemplo para un usuario con “rol admin” podra hacerlas todas, pero un usuario con “rol guest” solo podra listar y visualizar información de los usuarios.

Todo esto va gestionado a traves de relaciones entre enitdades que representan tablas en la BD. Esto lo explicare en otro post con mas detalle para que veais como he resuelto la gestion del acceso de usuarios con roles, permisos y zonas en una aplicacion con FOSUserBundle de por medio.

Empezamos:

1- He definido en el firewall que el rol minimo para poder acceder a esa ruta debe ser algo parecido a esto, de esa forma otros usuarios que no tengan ese rol “minimo” no podran acceder a esa/s ruta/s:

(security.yml)
access_control:
    - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - ... //seguridad de acceso en otras rutas
    - { path: ^/users, roles: ROLE_GUEST }

2 – En mi bundle he creado una carpeta llamada “Security” donde metere los Voters para revisar la seguridad de las acciones sobre las entidades.

3 – Como en la gran mayoria de mis Voters van a hacerse las mismas (o parecidas) comprobaciones respecto a si puede acceder a ese recurso o no, me he hecho un “GenericVoter” abstracto donde se incluye la logica de la seguridad, el cual herederan el resto de Voters de mis zonas:

<?php
namespace efor\AppBundle\Security;

use Doctrine\Common\Collections\Criteria;
use efor\AppBundle\Entity\Role;
use efor\AppBundle\Entity\RoleZone;
use efor\UsuarioBundle\Entity\User;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter;
use Symfony\Component\Security\Core\User\UserInterface;

// La clase AbstractVoter está disponible a partir de Symfony 2.6
abstract class GenericVoter extends AbstractVoter
{
    const READ = 'read';
    const CREATE = 'create';
    const EDIT = 'edit';
    const DELETE = 'delete';
    
    protected function getSupportedAttributes()
    {
        return array(self::CREATE, self::EDIT, self::DELETE, self::READ);
    }

    abstract protected function getSecureZone();

    /**
     * @param string $action
     * @param object $object
     * @param null $user
     *
     * @return bool
     */
    protected function isGranted($action, $object, $user = null)
    {
        if(!$user instanceof User){
            return false;
        }

        //Buscamos todos los roles del usuario(>= 1 role)
        $roleUser = $user->getRoleUser();

        //recorremos los posibles roles que tenga el usuario
        /** @var Role $role */
        foreach($roleUser as $role){ 
            //buscamos las zonas que tenga disponibles ese role
            $securedZones = $role->getRoleZone(); 

            /** @var RoleZone $securedZone */
            foreach($securedZones as $securedZone)
            {
                if($securedZone->getZone()->getName() === $this->getSecureZone()){
                    switch($action){
                        case self::READ:
                                return $securedZone->getAllowedToRead();
                            break;
                        case self::CREATE:
                                return $securedZone->getAllowedToAdd();
                            break;
                        case self::EDIT:
                                return $securedZone->getAllowedToEdit();
                            break;
                        case self::DELETE:
                                return $securedZone->getAllowedToDelete();
                            break;
                        default: return false;
                            break;
                    }
                }
            }
        }

        return true; //dejamos pasar siempre que se pueda
    }
}

En esta clase hay que destacar que hereda de AbstractVoter que definde una serie de metodos publicos implementados, y otros abstractos. De esa forma, asi nosotros añadimos las posibles acciones sobre un “objeto (entidad)” y una funcion abstracta getSecureZone() donde cada Voter especifico definira la “zona” donde se revisara la seguridad. Dentro de la funcion isGranted() comprobamos que esa zona este definida en alguno de los roles del usuario que la intenta acceder para ver si puede leer, crear, editar o eliminar un objeto de ese tipo.

4 – Definimos nuestro Voter especifico para comprobar la seguridad cuando se accede a los usuarios tal que así:

class UserVoter extends GenericVoter
{
    protected function getSupportedClasses()
    {
        return array('efor\UsuarioBundle\Entity\User');
    }

    protected function getSecureZone()
    {
        //en mi caso apunta a una clase donde tengo definidas 
        //mis rutas seguras - user
        return ZoneMapping::USERS_ROUTE_NAME;
    }
}

Este Voter comprobara para la entidad User (es la clase donde se podran hacer las 4 operaciones posibles – CRUD) cuando acceda a la zona ‘user’ si es posible conceder el acceso.

5 – Definimos un servicio en nuestro ‘services.yml’ que atendera para garantizar la seguridad:

services:
    ...
    user_voter:
        class: efor\AppBundle\Security\UserVoter
        public: false
        tags:
            - { name: security.voter }

6 – Ahora tenemos que usar las funciones que proporciona Symfony de chequeo del acceso para que este Voter se ejecute como responsable. Para ello en nuestro UserController deberemos añadir la/s siguiente/s comprobaciones a traves de anotaciones(cuando se pueda) o condicionales:

class UserController
 {
/**
 * @Route("/create", name="user_create")
 */
public function createUserAction(Request $request)
{
     $newUser = new User();

     if(!$this->isGranted(GenericVoter::CREATE, $newUser)){
         throw $this->createAccessDeniedException();
     }
     $form = ...
}
/**
 * @Route("/list", name="user_list")
 */
public function listUserAction()
{
    if(!$this->isGranted(GenericVoter::READ, new User())){
        throw $this->createAccessDeniedException();
    }
    ...
}

/**
* @Route("/show/{id}", name="user_show")
* @ParamConverter("user", class="efor\UsuarioBundle\Entity\User")
* @Security("is_granted('read', user)")
*/
public function showUserAction(User $user)
{
    if($user === $this->getUser()){
        return $this->redirectToRoute('profile_show');
    }

    return $this->render('UsuarioBundle:User:showUser.html.twig', array(
        'user' => $user
    ));
}

//IDEM con el resto de acciones del CRUD o Actions 
//que queramos proteger

Por una parte destacamos la anotacion @Security donde le decimos que ejecute la funcion isGranted y le pasamos la accion y un objeto(que se obtiene con el @ParamConverter previo).

De la misma forma pero sin usar anotacion, es llamando dentro de nuestro Action a $this->isGranted() pasandole la misma informacion ( a veces necesitamos instanciar un nuevo objeto para que se pueda comprobar) y lanzando una excepcion en caso negativo.

7 – Si queremos revisar la seguridad en las vistas para mostrar u ocultar botones podemos hacer uso de la funcion is_granted() en TWIG de la siguiente forma:

{% set editAction = constant('efor\\AppBundle\\Security\\GenericVoter::EDIT') %}
{% if is_granted(editAction, user) %}
    <a href={{ url('user_edit', { 'id': user.id } }}>Editar</a>
{% endif %}

De esta forma mostrar un link a la ruta de editar usuario solo si mi Voter lo permite validando los permisos sobre ese objeto.


 

Y eso es todo, podeis encontrar mas informacion sobre los Voters en el link que os deje al principio del post para que se ajuste a vuestras necesidades.

Si te ha gustado el articulo, no dudes en compartirlo en las redes sociales para que llegue a toda la comunidad Symfony Hispana. Si tienes otras formas de usarlo, o crees que harias algo distinto y que aportara valor a los demas, no dudes en dejar tu comentario al respecto, y lo revisare lo antes que pueda.

Saludos ¡

 

 

 

Limitar el acceso a las vistas del FOSUserBundle de un usuario logeado

Uno de los problemas que más suele ocurrir cuando usamos FOSUserBundle en nuestros proyectos, es que el usuario puede acceder a las rutas de login, registro, etc. una vez esta logeado y en la gran mayoria de los casos, no queremos que tenga acceso porque ya no lo necesita o porque no procede en la logica de negocio de la aplicación.

Para prevenir justamente esto, he desarrollado una solucion que me parece bastante sencilla y que hace justamente lo que queremos sin complicaciones.

Para ello, creamos un nuevo controlador llamado SecurityController que hereda del SecurityController del FOSUserBundle dentro de nuestro bundle de Usuarios, y creamos un nuevo loginAction que sobrescribe y atiende a la ruta del FOSUserBundle, de la siguiente forma:

use...
class SecurityController extends \FOS\UserBundle\Controller\SecurityController
{
/**
 * @param Request $request
 * @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
 *
 * @Route("/login", name="login")
 */
public function loginAction(Request $request)
{
 if($this->getUser() instanceof User){
     return $this->redirect($this->generateUrl('dashboard_homepage'));
 }
 return parent::loginAction($request);
}
}

De esta forma cuando se intente acceder al login, se comprueba si esta logeado, y se redirige a la homepage o por el contrario, llamamos al login action del FOSUserBundle el cual nos devuelve la vista del login con sus parametros y demás.

Esto mismo se puede extrapolar al registro o otras acciones que no queramos que el usuario acceda una vez se a logeado satisfactoriamente.

Me parece una solucion sencilla y elegante y que no se acopla, sino que si en algun momento actualizan las acciones del FOSUserBundle, seguiremos teniendo la logica del bundle intacta.

En otros sitios, comentan de hacer un copy – paste del codigo del action y adaptarlo a nuestra logica de negocio, pero esa solucion es mala, ya que si cambian algo de los “action” del bundle en futuras actualizaciones del bundle, estamos muy pero que muy “jodidos”.

Compartid por favor ¡¡ Y felices fiestas ¡¡