Validación y autorización en arquitectura en capas.

13

Sé que estás pensando (o tal vez gritando), "¿no es otra pregunta preguntando dónde pertenece la validación en una arquitectura en capas?!?" Bueno, sí, pero espero que esto sea un poco diferente al tema.

Creo firmemente que la validación toma muchas formas, está basada en el contexto y varía en cada nivel de la arquitectura. Esa es la base para la publicación: ayudar a identificar qué tipo de validación se debe realizar en cada capa. Además, una pregunta que a menudo surge es dónde pertenecen las verificaciones de autorización.

El escenario de ejemplo proviene de una aplicación para un negocio de catering. Periódicamente durante el día, un conductor puede entregar a la oficina cualquier exceso de efectivo que haya acumulado mientras lleva el camión de un sitio a otro. La aplicación permite al usuario registrar la "caída de efectivo" mediante la recopilación de la identificación del conductor y la cantidad. Aquí hay un código esqueleto para ilustrar las capas involucradas:

public class CashDropApi  // This is in the Service Facade Layer
{
    [WebInvoke(Method = "POST")]
    public void AddCashDrop(NewCashDropContract contract)
    {
        // 1
        Service.AddCashDrop(contract.Amount, contract.DriverId);
    }
}

public class CashDropService  // This is the Application Service in the Domain Layer
{
    public void AddCashDrop(Decimal amount, Int32 driverId)
    {
        // 2
        CommandBus.Send(new AddCashDropCommand(amount, driverId));
    }
}

internal class AddCashDropCommand  // This is a command object in Domain Layer
{
    public AddCashDropCommand(Decimal amount, Int32 driverId)
    {
        // 3
        Amount = amount;
        DriverId = driverId;
    }

    public Decimal Amount { get; private set; }
    public Int32 DriverId { get; private set; }
}

internal class AddCashDropCommandHandler : IHandle<AddCashDropCommand>
{
    internal ICashDropFactory Factory { get; set; }       // Set by IoC container
    internal ICashDropRepository CashDrops { get; set; }  // Set by IoC container
    internal IEmployeeRepository Employees { get; set; }  // Set by IoC container

    public void Handle(AddCashDropCommand command)
    {
        // 4
        var driver = Employees.GetById(command.DriverId);
        // 5
        var authorizedBy = CurrentUser as Employee;
        // 6
        var cashDrop = Factory.CreateCashDrop(command.Amount, driver, authorizedBy);
        // 7
        CashDrops.Add(cashDrop);
    }
}

public class CashDropFactory
{
    public CashDrop CreateCashDrop(Decimal amount, Employee driver, Employee authorizedBy)
    {
        // 8
        return new CashDrop(amount, driver, authorizedBy, DateTime.Now);
    }
}

public class CashDrop  // The domain object (entity)
{
    public CashDrop(Decimal amount, Employee driver, Employee authorizedBy, DateTime at)
    {
        // 9
        ...
    }
}

public class CashDropRepository // The implementation is in the Data Access Layer
{
    public void Add(CashDrop item)
    {
        // 10
        ...
    }
}

He indicado 10 ubicaciones donde he visto verificaciones de validación colocadas en el código. Mi pregunta es qué verificaciones realizaría, si las hubiera, en cada una de las siguientes reglas comerciales (junto con verificaciones estándar de longitud, rango, formato, tipo, etc.):

  1. El monto de la caída de efectivo debe ser mayor que cero.
  2. La caída de efectivo debe tener un controlador válido.
  3. El usuario actual debe estar autorizado para agregar caídas de efectivo (el usuario actual no es el conductor).

Por favor comparta sus pensamientos, cómo tiene o cómo abordaría este escenario y los motivos de sus elecciones.

Hijo de pirata
fuente
SE no es exactamente la plataforma correcta para "fomentar una discusión teórica y subjetiva". Votación para cerrar.
tdammers
Declaración mal redactada. Realmente estoy buscando las mejores prácticas.
SonOfPirate
2
@tdammers - Sí, es el lugar correcto. Al menos eso quiere ser. De las preguntas frecuentes: 'Se permiten preguntas subjetivas'. Es por eso que crearon este sitio en lugar de Stack Overflow. No seas un nazi cercano. Si la pregunta apesta, se desvanecerá en la oscuridad.
FastAl
@FastAI: No es tanto la parte 'subjetiva' sino la 'discusión' lo que me molesta.
tdammers
Creo que podría aprovechar los objetos de valor aquí al tener un CashDropAmountobjeto de valor en lugar de usar un Decimal. Verificar si el controlador existe o no se haría en el controlador de comandos y lo mismo ocurre con las reglas de autorización. Puede obtener una autorización de forma gratuita haciendo algo como Approver approver = approverService.findById(employeeId)lo que arroja si el empleado no está en el rol de aprobador. Approversolo sería un objeto de valor, no una entidad. También podría deshacerse de su método de fábrica de la fábrica o el uso de un AR lugar: cashDrop = driver.dropCash(...).
plalx

Respuestas:

2

Estoy de acuerdo en que lo que está validando será diferente en cada capa de la aplicación. Por lo general, solo valido lo que se requiere para ejecutar el código en el método actual. Intento tratar los componentes subyacentes como cajas negras y no valido en función de cómo se implementan esos componentes.

Entonces, como ejemplo, en su clase CashDropApi, solo verificaría que el 'contrato' no sea nulo. Esto evita NullReferenceExceptions y es todo lo que se necesita para garantizar que este método se ejecute correctamente.

No sé si validaría nada en las clases de servicio o comando y el controlador solo verificaría que 'comando' no sea nulo por las mismas razones que en la clase CashDropApi. He visto (y hecho) validación en ambos sentidos wrt a las clases de fábrica y entidad. Uno u otro es donde desea validar el valor de "cantidad" y que los otros parámetros no son nulos (sus reglas de negocio).

El repositorio solo debe validar que los datos contenidos en el objeto son consistentes con el esquema definido en su base de datos y la operación daa tendrá éxito. Por ejemplo, si tiene una columna que no puede ser nula o tiene una longitud máxima, etc.

En cuanto al control de seguridad, creo que es realmente una cuestión de intención. Dado que la norma tiene como objetivo evitar el acceso no autorizado, me gustaría hacer esta verificación lo antes posible en el proceso para reducir la cantidad de pasos innecesarios que he tomado si el usuario no está autorizado. Probablemente lo pondría en el CashDropApi.

jpm70
fuente
1

Tu primera regla de negocios

El monto de la caída de efectivo debe ser mayor que cero.

parece un invariante de tu CashDropentidad y de tu AddCashDropCommandclase. Hay un par de formas en que hago cumplir una invariante como esta:

  1. Tome la ruta Diseño por contrato y use Contratos de código con una combinación de Precondiciones, Postcondiciones y un [Método de contrato variable] según su caso.
  2. Escriba código explícito en el constructor / setters que arroje una excepción ArgumentException si pasa una cantidad menor que 0.

Su segunda regla es de naturaleza más amplia (a la luz de los detalles en la pregunta): válido significa que la entidad del Conductor tiene una bandera que indica que puede conducir (es decir, que no se suspendió su licencia de conducir), significa que el conductor fue realmente trabajando ese día o significa simplemente que el driverId, pasado a CashDropApi, es válido en el almacén de persistencia.

En cualquiera de estos casos, deberá navegar por su modelo de dominio y obtener la Driverinstancia de su IEmployeeRepository, como lo hace location 4en su ejemplo de código. Entonces, aquí debe asegurarse de que la llamada al repositorio no devuelva nulo, en cuyo caso su ID de controlador no era válido y no puede continuar con el procesamiento.

Para las otras 2 verificaciones (mi hipotética) (si el conductor tiene una licencia de conducir válida, si el conductor trabajaba hoy), está ejecutando reglas comerciales.

Lo que tiendo a hacer aquí es usar una colección de clases de validación que operan en entidades (al igual que el patrón de especificación del libro de Eric Evans - Diseño controlado por dominio). He usado FluentValidation para construir estas reglas y validadores. Entonces puedo componer (y, por lo tanto, reutilizar) reglas más complejas / más completas a partir de reglas más simples. Y puedo decidir qué capas de mi arquitectura ejecutarlas. Pero los tengo todos codificados en un solo lugar, no dispersos por todo el sistema.

Su tercera regla se refiere a una preocupación transversal: la autorización. Como ya está utilizando un contenedor de IoC (suponiendo que su contenedor de IoC admite la intercepción de métodos), puede hacer algo de AOP . Escriba un documento que haga la autorización y puede usar su contenedor IoC para inyectar este comportamiento de autorización donde sea necesario. La gran victoria aquí es que ha escrito la lógica una vez, pero puede reutilizarla en su sistema.

Para utilizar la intercepción a través de un proxy dinámico (Castle Windsor, Spring.NET, Ninject 3.0, etc.), su clase de destino necesita implementar una interfaz o heredar de una clase base. Debería interceptar antes de la llamada al método de destino, verificar la autorización del usuario y evitar que la llamada continúe con el método real (arrojar una exclusión, registro, devolver un valor que indica un error u otra cosa) si el usuario no tiene Los roles correctos para realizar la operación.

En su caso, puede interceptar la llamada a

CashDropService.AddCashDrop(...) 

AddCashDropCommandHandler.Handle(...)

Los problemas aquí tal vez CashDropServiceno pueden ser interceptados porque no hay una interfaz / clase base. O AddCashDropCommandHandlerno está siendo creado por su IoC, por lo tanto, su IoC no puede crear un proxy dinámico para interceptar la llamada. Spring.NET tiene una característica útil en la que puede orientar un método en una clase en un ensamblaje a través de una expresión regular, por lo que esto puede funcionar.

Espero que esto te dé algunas ideas.

RobertMS
fuente
¿Puede explicar cómo "usaría su contenedor IoC para inyectar este comportamiento de autorización donde debe estar"? Esto suena atractivo, pero lograr que AOP e IoC trabajen juntos se me escapa hasta ahora.
SonOfPirate
En cuanto al resto, estoy de acuerdo con colocar la validación en el constructor y / o establecedores para evitar que el objeto entre en un estado no válido (manejo de invariantes). Pero más allá de eso y una referencia a la verificación nula después de ir al IEmployeeRepository para ubicar el controlador, no proporciona ningún detalle donde realizaría el resto de la validación. Dado el uso de FluentValidation y la reutilización, etc. que proporciona, ¿dónde aplicaría las reglas en el modelo dado?
SonOfPirate
He editado mi respuesta, vea si esto ayuda. En cuanto a "¿dónde aplicarías las reglas en el modelo dado?"; probablemente alrededor de 4, 5, 6, 7 en su controlador de comandos. Tiene acceso a los repositorios que pueden proporcionar la información que necesita para realizar la validación a nivel empresarial. Pero creo que hay otros que no estarían de acuerdo conmigo aquí.
RobertMS
Para aclarar, se están inyectando todas las dependencias. Lo dejé para mantener breve el código de referencia. Mi consulta tiene más que ver con tener una dependencia dentro del aspecto, ya que los aspectos no se inyectan a través del contenedor. Entonces, ¿cómo obtiene AuthorizationAspect una referencia al AuthorizationService, por ejemplo?
SonOfPirate
1

Para las reglas:

1- El monto de la caída de efectivo debe ser mayor que cero.

2- La caída de efectivo debe tener un controlador válido.

3- El usuario actual debe estar autorizado para agregar gotas de efectivo (el usuario actual no es el conductor).

Haría la validación en la ubicación (1) para la regla comercial (1) y me aseguraría de que el Id no sea nulo o negativo (suponiendo que cero sea válido) como verificación previa para la regla (2). La razón es mi regla de "No cruce el límite de una capa con datos incorrectos que puede verificar con la información disponible". Una excepción a esto sería si el servicio realiza la validación como parte de su deber para con otras personas que llaman. En cuyo caso, será suficiente tener la validación solo allí.

Para las reglas (2) y (3), esto debe hacerse en la capa de acceso a la base de datos (o en la capa db en sí) solo porque implica el acceso db. No es necesario viajar entre capas intencionalmente.

En particular, la regla (3) se puede evitar si dejamos que la GUI evite que usuarios no autorizados presionen el botón que permite este escenario. Si bien esto es más difícil de codificar, es mejor.

¡Buena pregunta!

Ninguna posibilidad
fuente
+1 para autorización: ponerlo en la interfaz de usuario es una alternativa que no mencioné en mi respuesta.
RobertMS
Si bien tener verificaciones de autorización en la interfaz de usuario proporciona una experiencia más interactiva para el usuario, estoy desarrollando una API basada en servicios y no puedo hacer ninguna suposición sobre qué reglas ha implementado o no la persona que llama. Debido a que muchas de estas comprobaciones se pueden delegar fácilmente en la interfaz de usuario, elegí usar el proyecto API como base para la publicación. Estoy buscando mejores prácticas en lugar de libros de texto de forma rápida y fácil.
SonOfPirate
@SonOfPirate, INMO, la interfaz de usuario debe hacer validaciones porque es más rápida y tiene más datos que el servicio (en algunos casos). Ahora el servicio no debe enviar datos fuera de sus límites sin hacer sus propias validaciones, ya que esto es parte de sus responsabilidades siempre que desee que el servicio no confíe en el cliente. En consecuencia, sugiero que se realicen verificaciones no db en el servicio (nuevamente) antes de enviar datos a la base de datos para su posterior procesamiento.
NoChance