Validación y autorización en arquitectura en capas

13

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

Creo firmemente que la validación toma muchas formas, se basa en el contexto y varía en cada nivel de la arquitectura. Esa es la base para el post - ayudar a identificar qué tipo de validación debe realizarse en cada capa. Además, una pregunta que surge a menudo es a dónde pertenecen las comprobaciones 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 al llevar el camión de un sitio a otro. La aplicación le permite a un usuario registrar la "caída de efectivo" al recopilar 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é controles, si los hubiera, realizarían en cada una de las siguientes reglas comerciales (junto con los controles 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 ideas, cómo ha abordado este escenario y las razones de sus elecciones.

    
pregunta SonOfPirate 19.04.2012 - 21:53

3 respuestas

2

Estoy de acuerdo en que lo que está validando será diferente en cada capa de la aplicación. Normalmente 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 validar según 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 cualquier cosa en las clases de servicio o comando y el manejador solo verificaría que el 'comando' no sea nulo por las mismas razones que en la clase CashDropApi. He visto (y hecho) la validación en ambos sentidos con las clases de fábrica y entidad. Uno u otro es donde querría validar el valor de 'cantidad' y que los otros parámetros no son nulos (sus reglas comerciales).

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 será exitosa. 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 regla pretende evitar el acceso no autorizado, me gustaría realizar esta comprobació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.

    
respondido por el jpm70 20.04.2012 - 14:24
1

Tu primera regla de negocios

  

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

parece un invariante de su entidad CashDrop y de su clase AddCashDropCommand . Hay un par de formas en las que hago cumplir una invariante como esta:

  1. Tome la ruta de Diseño por contrato y use el código Contratos con una combinación de condiciones previas, condiciones posteriores y una [ContractInvariantMethod] dependiendo de su caso.
  2. Escriba código explícito en el constructor / definidores que lanza un ArgumentException si pasa una cantidad menor a 0.

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

En cualquiera de estos casos, tendrá que navegar por su modelo de dominio y obtener la instancia Driver de su IEmployeeRepository , como lo hace en location 4 en su ejemplo de código. Por lo tanto, aquí debe asegurarse de que la llamada al repositorio no devuelva un valor nulo, en cuyo caso su DriverId no era válido y no puede continuar con el procesamiento.

Para las otras 2 (mis hipotéticas) verificaciones (¿el conductor tiene una licencia de conducir válida? ¿Funcionó el conductor hoy?) está ejecutando reglas de negocios.

Lo que tiendo a hacer aquí es usar una colección de clases de validador que operan en entidades (como el patrón especificación del libro de Eric Evans - Domain Driven Design). He utilizado FluentValidation para crear 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 relaciona con una preocupación transversal: la autorización. Como ya está utilizando un contenedor IoC (asumiendo que su contenedor IoC admite la intercepción de métodos) puede hacer algo de AOP . Escriba un aspecto que haga la autorización y puede usar su contenedor de IoC para inyectar este comportamiento de autorización donde sea necesario. La gran ventaja aquí es que ha escrito la lógica una vez, pero puede reutilizarla en todo su sistema.

Para usar la intercepción a través de un proxy dinámico (Castle Windsor, Spring.NET, Ninject 3.0, etc.) su clase objetivo necesita implementar una interfaz o heredar de una clase base. Interceptaría antes de la llamada al método de destino, verificaría la autorización del usuario y evitaría que la llamada continúe con el método real (arroje un extracto, registre, devuelva un valor que indique un error o algo más) si el usuario no tiene los roles correctos para realizar la operación.

En su caso, podría interceptar la llamada a cualquiera de ellos

CashDropService.AddCashDrop(...) 

AddCashDropCommandHandler.Handle(...)

Los problemas aquí tal vez que CashDropService no puede ser interceptado porque no hay una interfaz / clase base. O bien, su IoC no está creando AddCashDropCommandHandler , por lo que su IoC no puede crear un proxy dinámico para interceptar la llamada. Spring.NET tiene una característica útil donde puedes apuntar 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.

    
respondido por el RobertMS 15.06.2012 - 00:08
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 caídas de efectivo (el usuario actual no es el conductor).

Haría la validación en la ubicación (1) de la regla de negocios (1) y me aseguraría de que la Id. no sea nula o negativa (suponiendo que el cero sea válido) como verificación previa de la regla (2). La razón es mi regla de "No cruzar un límite de capa con datos incorrectos que pueda 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 hay necesidad de viajar entre capas intencionalmente.

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

Buena pregunta!

    
respondido por el NoChance 02.07.2012 - 17:08

Lea otras preguntas en las etiquetas