Skip to content

gmarti28/challenge-fintech-cl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

challenge-fintech-cl

Prerequisitos

  • Docker Desktop y docker-compose (se incluye con Docker Desktop) instalados y disponibles
  • JDK 11 (Ej: AdoptOpenJDK 11). Se puede instalar en linux y mac con SdkMan
    • Se recomienda instalar un SDK Hotspot aunque para este challenge la cantidad de ejecuciones de los métodos no hará diferencia en la performance debido a que los JDK hotspot compilan el bytecode a nativo luego de 10000 ejecuciones del mismo método.
    • Para instalar con SdkMan sdk install java 11.0.11.hs-adpt
  • Opcional: Maven. (El proyecto incluye Maven Wrapper)

Ejecución con Docker

Encendido

local-dev/compose-up.sh

Esa sentencia utiliza docker-compose (ver local-dev/docker-compose.yml) para:

  • Iniciar un contenedor de Postgres a partir de una imagen pública que se customiza con un script incluido en la ejecución
  • Inicializar el esquema "desafio" de la base utilizando liquibase. Este esquema se usa para Spring Security dado que la autogeneración del modelo en el framework no es compatible con el dialecto de Postgres (funciona sólo con H2)
  • El contenedor de liquibase cumple su función y finaliza.
  • Compilar la aplicación en un layer intermedio de docker, utilizando mvnw (Maven Wrapper)
  • Iniciar un contenedor que escucha en el puerto 8080 a partir de la imagen de la aplicación compilada en el paso anterior.

El enfoque usado para construir la aplicación y sus dependencias con docker-compose no son production-ready ya que se desconocen los mecanismos de CI/CD y los lineamientos para definir variables de entorno y secrets. Por ejemplo sops, Helm, etc.

Apagado del servicio y su dependencia

local-dev/compose-down.sh

Acceso a Swagger UI

Endpoints disponibles

Metodo URL Segurizado Uso
POST /signup NO Registro de nuevos usuarios
POST /login NO Login de usuarios con usuario y contraseña
POST /math/add SI Servicio para sumar dos números
GET /audit SI Consulta paginada de acceso a los endpoints
POST /logout SI Desloggeo de usuarios

Además existe un endpoint oculto (autogenerado por Spring Security) /api/logout que no genera entradas en la base de auditoría. Esto se debe a que Spring lo implementa sin respetar los ejecución de la filter chain

Al usar el endpoint de /login el server configura una cookie JSESSIONID httpOnly que se puede enviar en los requests posteriores para mantener la sesión del usuario. El endpoint de /logout invalida la sesión.

Para todos los requests que requieren estar autenticado se puede incluir un header de Authorization con formato username:password o su base64

Pruebas por Postman

  • Se pueden ejercitar los distintos endpoints de la aplicación importando la collection postman/Tenpo.postman_collection.json
  • Esta collection contiene Test Scripts que setean variables de entorno. Siguiendo buenas prácticas no se guardan como variables globales sino que se establecen como variables de entorno.
  • Por tal motivo la collection necesita ejecutarse con un Environment (se puede importar el environment vacío desde la carpeta postman/Local.postman_environment.json)
  • Esta collection dá de alta usuarios con usernames generados al azar y contraseña 'password'
  • Además se provee un primer usuario pre-inicializado desde liquibase ("user/password")

Ejecución desde un IDE

La configuración de JPA se encuentra en el archivo src/main/resources/application.yml y es importante entender que la URL de Postgres suministrada sólo funciona iniciando la aplicación con docker-compose:

  datasource:
    url: jdbc:postgresql://postgres:5432/desafio

Nótese que se suministra como hostname "postgres" el cual no es un hostname real (resoluble por dns) sino el nombre del container de Postgres resoluble gracias al mecanismo de networking de Docker Compose. Véase Compose networking para mas información.

Para ejecutar o depurar la aplicación desde el IDE se necesita:

  1. Editar el hostname de postgres en application.yml (por ejemplo localhost:5432)
  2. Comentar con # las líneas en donde se define el servicio "application:" en local-dev/docker-compose.yml
  3. Iniciar la base de datos con local-dev/compose-up.sh
  4. Ejecutar en el IDE el método main de la clase src/main/java/com/gastonmartin/desafio/DesafioApplication.java

Consideraciones de la implementación

CSRF - Cross Site Request Forgery

En sistemas productivos donde el backend se utiliza desde otra aplicación (una UI) resulta crucial tener un mecanismo de protección contra los ataques de CSRF/XSRF en donde un script malicioso alojado en un sitio malicioso redirecciona al navegador de la víctima a un recurso de nuestro backend sin que la víctima lo detecte, personificándola a través de las cookies guardadas en el navegador. El mecanismo estándar de protección consiste en generar un TOKEN del lado del backend asociado a la sesión del usuario, configurarlo como una cookie httpOnly (no legible desde ningún script en el cliente) que debe ser enviado junto con los request subsiguientes en forma de Cookie o Header para que el backend acepte los requests. En Spring Security se implementa muy facilmente, pero dificulta las pruebas desde Swagger (no asi con Postman)

En la consigna del challenge no se pide la adopción de medidas de protección contra CSRF, pero de ser necesarias puedo agregarlas si me lo indican.

CORS - Cross Origin Resource Sharing

Debido a que no se implementa una UI para este backend, y se desconoce el Origin del cual provendrían los requests, no se implementó ninguna protección CORS. De hacerlo hubiese tenido que indicar un Access-Control-Allow-Origin permisivo (*)

JWT - Json Web Token

En entornos productivos donde se requiere facilitar la escalabilidad horizontal, y para evitar mantener y sincronizar la sesión entre multiples instancias del mismo servicio, se recomienda implementar JWT Token.

Cuando se usa JWT el backend en vez de guardar y sincronizar la información de la sesión la describe en una estructura JSON. Esta estructura puede contener información de autenticación y de autorización, como ser username, roles, autorizaciones, tiempo de expiración, etc.) y esta información se encodea con base64 para simplificar el intercambio. Luego el backend firma criptográficamente el header y body del JSON con un secret conocido únicamente por el backend, lo cual evita que cualquier cliente pueda alterar (tampering) los claims incluidos en el JSON (por ejemplo impersonando a otro usuario diferente, alterando los roles, etc.) dado que de hacerlo la firma no coincidiría y el backend lo detectaria. La adopción de un JWT Token puede volver innecesaria la adopción de un token CSRF bajo ciertas precauciones (tiempos de expiración de la sesión razonablemente cortos)

Configuracion de Spring Security

La seguridad se maneja con Spring Security, usando Bcrypt como PasswordEncoder y la implementación JdbcUserDetailsManager de UserDetailsService delegada en la base de datos Postgres. Esta configuración se puede ver en el archivo src/main/java/com/gastonmartin/desafio/config/ProjectSecurityConfig.java

Dado que el mecanismo withDefaultSchema() de AuthenticationManagerBuilder genera DDL inválido para Postgres, se utiliza Liquibase para inicializar las tablas de USERS y AUTHORITIES requeridas por Spring Security.

Para facilitar el testing inicial de la aplicación se pre generan credenciales (user/password) para un usuario mediante Liquibase.

Se definió un solo rol de seguridad "ROLE_USER" porque no se tiene mayor cantidad de endpoints que haga necesaria una separación más fina.

Se restringió el acceso a algunos recursos usando MvcMatcher por considerarse mas seguro que AntMatcher. Se definió que todo recurso no enlistado en la configuración no sea accesible. Algunos recursos se pueden acceder sin estar autenticado (por razones lógicas)

Debido al requerimiento de mantener un registro de todos los endpoints accedidos se implementó un AuditRequestsFilter que guarda los accesos a los endpoints en la base de datos. El modelo de datos para este mecanismo se configura automáticamente con Hibernate. Se podría haber delegado enteramente en los scripts de liquibase desactivando la funcionalidad de actualización automática de Hibernate.

Esta funcionalidad se implementó usando JPA Repository con métodos autogenerados por la interfaz AuditRepository. Me apalanqué en la abstracción Pageable de Spring Data para devolver el resultado paginado. Podría haber implementado una lógica más liviana pidiendo parámetros offset y limit, pero me pareció una reinvención innecesaria.

Asimismo la implementación de la auditoría mediante un filtro de Spring agregado a la Filter Chain se podría haber reemplazado por un bean autowired al servicio o al repositorio de AuditRepository que actualice la tabla al ingresar o salir del controller (la lógica no se especificó en la consigna). La implementación en un filtro me pareció más concisa y elegante.

Debido a que los filtros no tienen acceso al Autowiring de Spring, tuve que implementar una lógica custom de acceso al contexto de Spring dentro del filtro. Además el filtro se implementó como OncePerRequestFilter para garantizar que no se ejecute más de una vez por request.

No se configura la auditoría para todas las rutas ya que no me interesa auditar por ejemplo los accesos a Swagger.

Sobre la implementacion de Form Login

Spring Security provee una implementación default del formulario de login, para lo cual agrega un endpoint oculto (reconfigurable) de /login que solo acepta payload de tipo x-www-form-encoded. Mediante la property springdoc.show-login-endpoint = true se puede incluir a dicho endpoint en swagger, pero encontré un BUG: la definición no coincide con lo que el endpoint acepta. Swagger propone un payload de tipo JSON pero el endpoint solo acepta x-www-form-encoded por lo que no funciona desde swagger. (La configuración de Swagger se mantiene en un archivo open-api.properties separado de las properties de la aplicación.)

Por ese motivo, implementé un endpoint de /login propio, que me permite manejar mejor el comportamiento y la documentación. Otro problema del /login default implementado por Spring que se comparte con el de /logout es que no respeta los filters y me impedía registrar los logins y los logout. Es por ello que implemente una version propia de ambos dos.

Se ha configurado el botón "Authorize" en swagger que permite especificar credenciales para todos los endpoints que necesitan login. Swagger traduce esa información en http basic authentication incluyendo un header Authorization cuando se suministran credenciales. Si se intenta llamar a un endpoint segurizado desde swagger sin cargar las credenciales primero con dicho botón, aparecerá un popup pidiendo usuario y contraseña.

About

Demo app with Spring Security using Database

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published