6

Recientemente comenzamos a usar Doctrine 2.2 y partes de Zend Framework 2 en un esfuerzo por mejorar la organización, reducir la duplicación, entre otras cosas. Hoy, comencé a lanzar ideas para implementar una capa de servicio para actuar como intermediario entre nuestros controladores y las entidades de Doctrine.Acceso a los datos y seguridad en la capa de servicio (Doctrine & ZF)

En este momento, la mayoría de nuestra lógica reside en el controlador. Además, usamos un asistente de acción para probar ciertos permisos; sin embargo, se me ocurrió un nuevo enfoque después de implementar Zend \ Di. Empecé a crear modelos de servicio específicos de la entidad, que usan Zend \ Di para inyectar una instancia de EntityManager y los permisos del usuario actual.

El código del controlador es el siguiente:

class Project_DeleteController extends Webjawns_Controller_Action 
{ 
    public function init() 
    { 
     $this->_initJsonContext(); 
    } 

    public function indexAction() 
    { 
     $response = $this->_getAjaxResponse(); 

     $auditId = (int) $this->_getParam('audit_id'); 
     if (!$auditId) { 
      throw new DomainException('Audit ID required'); 
     } 

     /* @var $auditService Service\Audit */ 
     $auditService = $this->getDependencyInjector()->get('Service\Audit'); 

     try { 
      $auditService->delete($auditId); 
      $response->setStatusSuccess(); 
     } catch (Webjawns\Exception\SecurityException $e) { 
      $this->_noAuth(); 
     } catch (Webjawns\Exception\Exception $e) { 
      $response->setStatusFailure($e->getMessage()); 
     } 

     $response->sendResponse(); 
    } 
} 

Y un ejemplo de una de nuestras capas de servicios. El constructor toma dos parámetros: uno toma el EntityManager y el otro un objeto Entity \ UserAccess, inyectado por Zend \ Di.

namespace Service; 

use Webjawns\Service\Doctrine, 
    Webjawns\Exception; 

class Audit extends AbstractService 
{ 
    public function delete($auditId) 
    { 
     // Only account admins can delete audits 
     if (\Webjawns_Acl::ROLE_ACCT_ADMIN != $this->getUserAccess()->getAccessRole()) { 
      throw new Exception\SecurityException('Only account administrators can delete audits'); 
     } 

     $audit = $this->get($auditId); 

     if ($audit->getAuditStatus() !== \Entity\Audit::STATUS_IN_PROGRESS) { 
      throw new Exception\DomainException('Audits cannot be deleted once submitted for review'); 
     } 

     $em = $this->getEntityManager(); 
     $em->remove($audit); 
     $em->flush(); 
    } 

    /** 
    * @param integer $auditId 
    * @return \Entity\Audit 
    */ 
    public function get($auditId) 
    { 
     /* @var $audit \Entity\Audit */ 
     $audit = $this->getEntityManager()->find('Entity\Audit', $auditId); 
     if (null === $audit) { 
      throw new Exception\DomainException('Audit not found'); 
     } 

     if ($audit->getAccount()->getAccountId() != $this->getUserAccess()->getAccount()->getAccountId()) { 
      throw new Exception\SecurityException('User and audit accounts do not match'); 
     } 

     return $audit; 
    } 
} 
  1. Es este un patrón apropiado a utilizar para lo que estamos tratando de lograr?
  2. ¿Es una buena práctica tener la validación de permisos dentro de la capa de servicio como publicada?
  3. Según tengo entendido, la lógica de visualización aún reside en el controlador, lo que le da flexibilidad al modelo para usar en varios contextos (JSON, XML, HTML, etc.). ¿Pensamientos?

Estoy contento con la forma en que esto funciona hasta ahora, pero si alguien ve alguna desventaja de cómo estamos haciendo esto, por favor publique sus ideas.

+1

Sólo mis dos peniques en la autenticación.No creo que haya un camino correcto ni un camino equivocado, sin embargo, comencé a poner mi autenticación en la capa de servicio, pero luego la moví a mis controladores. Mi razonamiento era que la capa de servicio es mi API interna y debería usar mi capa de controlador para exponer eso al mundo, por lo tanto, debería decidir quién tiene acceso a qué. Además, si quiero construir herramientas/scripts internos, etc., no es necesario que construya la autenticación en ellos para usar mi capa de servicio. –

+0

Tenga cuidado de no mezclar autenticación y control de acceso. La autenticación puede (¿debería?) Entrar en el módulo, antes de que las clases de dominio entren en la imagen. Solo cuando hayas establecido la identidad del usuario. Ejemplo: if (! $ AuthService-> hasIdentity()) dentro de su modelo de servicio de dominio. – dualmon

+1

@JamieSutherland: No estoy de acuerdo. Los servicios definen la lógica comercial; los controladores son el puente entre las solicitudes y la lógica comercial apropiada. Por ejemplo, solo tendría un servicio único para ordenar un producto, pero podría tener múltiples controladores para solicitudes HTTP, solicitudes de API, etc. Si le preocupa que la ACL solicite información específica (por ejemplo, a través de HTTP, puede esperar una sesión de usuario en la que esperaría una clave secreta para las solicitudes de la API), generalice su implementación de ACL para permitir esto. – moteutsch

Respuesta

1

Me gusta lo que estás haciendo aquí, y creo que tu separación de preocupaciones es buena. Estamos experimentando con ir un paso más allá, utilizando repositorios personalizados. Así, por ejemplo, en un método de modelo/servicio estándar puede tener este aspecto:

public function findAll($sort = null) 
{ 
    if (!$sort) $sort = array('name' => 'asc'); 
    return $this->getEm()->getRepository('Application\Entity\PartType') 
       ->findAll($sort); 

} 

... estamos añadiendo cosas que requieren DQL al repositorio, para mantener toda DQL de los modelos, por ejemplo:

public function findAllProducts($sort = null) 
{ 
    if (!$sort) $sort = array('name' => 'asc'); 
    return $this->getEm()->getRepository('Application\Entity\PartType') 
       ->findAllProducts($sort); 

} 

para el modelo anterior, la clase repositorio es así:

<?php 
namespace Application\Repository; 

use Application\Entity\PartType; 
use Doctrine\ORM\EntityRepository; 

class PartTypeRepository extends EntityRepository 
{ 

    public function findAllProducts($order=NULL) 
    { 
     return $this->_em->createQuery(
        "SELECT p FROM Application\Entity\PartType p 
         WHERE p.productGroup IS NOT NULL 
         ORDER BY p.name" 
       )->getResult(); 
    } 

} 

Tenga en cuenta que sólo hemos ampliado Doctrine \ ORM \ EntityRepository lo que significa que no tenemos que volver a definir todo el métodos estándar de repositorio de Doctrine, pero podemos anularlos si es necesario y podemos agregar nuestros propios personalizados.

Por lo tanto, en lo que respecta al control de acceso, nos brinda la posibilidad de agregar restricciones basadas en identidad u otras condiciones de nivel de registro a un nivel muy bajo, accediendo a la lógica comercial en sus servicios desde el Repositorio. Al hacerlo de esta manera, los servicios desconocen la implementación. Mientras seamos estrictos acerca de no poner DQL en otras partes de la aplicación, podemos lograr restricciones comerciales de nivel récord para cualquier clase que acceda a la base de datos a través del repositorio. (Tenga cuidado con el DQL personalizado en niveles superiores de la aplicación).

Ejemplo:

public function findAll($order=NULL) 
    { 
     // assumes PHP 5.4 for trait to reduce boilerplate locator code 
     use authService; 

     if($this->hasIdentity()) { 
      return $this->_em->createQuery(
         "SELECT p FROM Application\Entity\PartType p 
          JOIN p.assignments a 
          WHERE a.id = " . $this->getIdentity()->getId() 
        )->getResult(); 
     } else { 
      return NULL; 
     } 
    } 
+1

Estoy totalmente en desacuerdo con colocar el código de control de acceso en el repositorio. El repositorio no debería saber nada sobre la lógica comercial de la aplicación. Debería preocuparse solo por recuperar datos. La lógica de negocios es para servicios y clases de alto nivel. – moteutsch

+0

Creo que también es una buena idea abstraer los repositorios de la implementación de Doctrine. Use la composición (de las clases predeterminadas del repositorio de Doctrine) en lugar de la herencia. Esto combinado con la codificación de una interfaz le permite intercambiar fácilmente las fuentes de datos en algunos o en todos sus repositorios (lo que me gusta llamar "mapeadores", para evitar confusiones con los repositorios de Doctrine). – moteutsch

Cuestiones relacionadas