mmmm
This commit is contained in:
parent
6b38075492
commit
dfc7633a1f
|
|
@ -11,6 +11,11 @@ SPRING_PROFILES_ACTIVE=postgres96
|
||||||
JWT_ISSUER_URI=http://keycloakic.ddns.net/auth/realms/controlms
|
JWT_ISSUER_URI=http://keycloakic.ddns.net/auth/realms/controlms
|
||||||
JWT_JWK_SET_URI=http://keycloakic.ddns.net/auth/realms/controlms/protocol/openid-connect/certs
|
JWT_JWK_SET_URI=http://keycloakic.ddns.net/auth/realms/controlms/protocol/openid-connect/certs
|
||||||
JWT_AUDIENCE=orquestador-nmp
|
JWT_AUDIENCE=orquestador-nmp
|
||||||
|
# POC sin Keycloak: compose.mock.yaml activa demo-token y reutiliza MOCK_ADMIN_TOKEN.
|
||||||
|
# Opcional: token independiente de 32..256 caracteres base64url (sin espacios).
|
||||||
|
# ORCHESTRATOR_DEMO_TOKEN=
|
||||||
|
# ORCHESTRATOR_DEMO_SUBJECT=poc-user
|
||||||
|
# ORCHESTRATOR_DEMO_SCOPE=demo
|
||||||
APP_BIND_ADDRESS=127.0.0.1
|
APP_BIND_ADDRESS=127.0.0.1
|
||||||
APP_PORT=8080
|
APP_PORT=8080
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ Incluye la estructura Maven, configuración JWT/PostgreSQL, Docker y salud verif
|
||||||
Para configurar respuestas de proveedores manualmente desde Postman, consultar
|
Para configurar respuestas de proveedores manualmente desde Postman, consultar
|
||||||
[Proveedores mock](demo/providers-mock/README.md). El archivo opcional
|
[Proveedores mock](demo/providers-mock/README.md). El archivo opcional
|
||||||
`compose.mock.yaml` añade el servicio HTTP de demo en `microsipNet`; el Compose
|
`compose.mock.yaml` añade el servicio HTTP de demo en `microsipNet`; el Compose
|
||||||
base conserva el servicio conectado a PostgreSQL externo.
|
base conserva el servicio conectado a PostgreSQL externo. La POC puede usar
|
||||||
|
[token fijo sin Keycloak](specs/POC_AUTH.md) mediante el perfil demo-token.
|
||||||
|
|
||||||
- Docker con contenedores Linux y Compose v2. La imagen compila con JDK 21 y Maven Wrapper; no requiere Java/Maven en el anfitrión.
|
- Docker con contenedores Linux y Compose v2. La imagen compila con JDK 21 y Maven Wrapper; no requiere Java/Maven en el anfitrión.
|
||||||
- PostgreSQL externo con base y usuario existentes, accesible desde Docker. El entorno actual usa PostgreSQL 9.6.2 en server02:30432 mediante el perfil postgres96; ver ADR-006.
|
- PostgreSQL externo con base y usuario existentes, accesible desde Docker. El entorno actual usa PostgreSQL 9.6.2 en server02:30432 mediante el perfil postgres96; ver ADR-006.
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,11 @@ services:
|
||||||
providers-mock:
|
providers-mock:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-postgres96},demo-mock
|
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-postgres96},demo-mock,demo-token
|
||||||
|
ORCHESTRATOR_DEMO_TOKEN: ${ORCHESTRATOR_DEMO_TOKEN:-${MOCK_ADMIN_TOKEN:?Configura MOCK_ADMIN_TOKEN}}
|
||||||
|
ORCHESTRATOR_DEMO_SUBJECT: ${ORCHESTRATOR_DEMO_SUBJECT:-poc-user}
|
||||||
|
ORCHESTRATOR_DEMO_SCOPE: ${ORCHESTRATOR_DEMO_SCOPE:-demo}
|
||||||
|
ADMIN_BUSINESS_SCOPES: ${ADMIN_BUSINESS_SCOPES:-${ORCHESTRATOR_DEMO_SCOPE:-demo}}
|
||||||
FLOW_CATALOG_PATH: /app/mock-config/publication-catalog.json
|
FLOW_CATALOG_PATH: /app/mock-config/publication-catalog.json
|
||||||
INTEGRATION_ROUTES_PATH: /app/mock-config/integration-routes.json
|
INTEGRATION_ROUTES_PATH: /app/mock-config/integration-routes.json
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ services:
|
||||||
DB_PASSWORD: ${DB_PASSWORD:?Configura DB_PASSWORD en .env}
|
DB_PASSWORD: ${DB_PASSWORD:?Configura DB_PASSWORD en .env}
|
||||||
DB_MIGRATION_USERNAME: ${DB_MIGRATION_USERNAME:-${DB_USERNAME}}
|
DB_MIGRATION_USERNAME: ${DB_MIGRATION_USERNAME:-${DB_USERNAME}}
|
||||||
DB_MIGRATION_PASSWORD: ${DB_MIGRATION_PASSWORD:-${DB_PASSWORD}}
|
DB_MIGRATION_PASSWORD: ${DB_MIGRATION_PASSWORD:-${DB_PASSWORD}}
|
||||||
JWT_ISSUER_URI: ${JWT_ISSUER_URI:?Configura JWT_ISSUER_URI en .env}
|
# Required by the application in normal mode; optional for the demo-token override.
|
||||||
JWT_JWK_SET_URI: ${JWT_JWK_SET_URI:?Configura JWT_JWK_SET_URI en .env}
|
JWT_ISSUER_URI: ${JWT_ISSUER_URI:-}
|
||||||
JWT_AUDIENCE: ${JWT_AUDIENCE:?Configura JWT_AUDIENCE en .env}
|
JWT_JWK_SET_URI: ${JWT_JWK_SET_URI:-}
|
||||||
|
JWT_AUDIENCE: ${JWT_AUDIENCE:-}
|
||||||
JWT_BUSINESS_SCOPE_CLAIM: ${JWT_BUSINESS_SCOPE_CLAIM:-business_scope}
|
JWT_BUSINESS_SCOPE_CLAIM: ${JWT_BUSINESS_SCOPE_CLAIM:-business_scope}
|
||||||
FLOW_ADMIN_ENABLED: ${FLOW_ADMIN_ENABLED:-false}
|
FLOW_ADMIN_ENABLED: ${FLOW_ADMIN_ENABLED:-false}
|
||||||
REQUEST_API_ENABLED: ${REQUEST_API_ENABLED:-false}
|
REQUEST_API_ENABLED: ${REQUEST_API_ENABLED:-false}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ MOCK_BIND_ADDRESS=0.0.0.0
|
||||||
MOCK_PORT=8788
|
MOCK_PORT=8788
|
||||||
```
|
```
|
||||||
|
|
||||||
El token es exclusivo del mock, no es un JWT de Keycloak. La publicación del
|
El token administra el mock y se reutiliza por defecto en el orquestador de POC
|
||||||
|
(ver POC_AUTH.md). No es un JWT de Keycloak. La publicación del
|
||||||
puerto en 0.0.0.0 permite acceso desde Postman en tu red de demo.
|
puerto en 0.0.0.0 permite acceso desde Postman en tu red de demo.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -75,7 +76,7 @@ cuerpos de hasta 256 KiB. Omisión de cabeceras restablece status=200 y delay=0.
|
||||||
|
|
||||||
El archivo base `compose.yaml` conserva un solo servicio. La combinación con
|
El archivo base `compose.yaml` conserva un solo servicio. La combinación con
|
||||||
`compose.mock.yaml` añade el mock a `microsipNet` y selecciona catálogo/rutas
|
`compose.mock.yaml` añade el mock a `microsipNet` y selecciona catálogo/rutas
|
||||||
sin sobrescribir `config/`. Activa el perfil `demo-mock` junto al perfil configurado
|
sin sobrescribir `config/`. Activa los perfiles `demo-mock` y `demo-token` junto al perfil configurado
|
||||||
(por defecto postgres96). Ese perfil permite HTTP únicamente hacia el nombre
|
(por defecto postgres96). Ese perfil permite HTTP únicamente hacia el nombre
|
||||||
`providers-mock` y puerto 8788. Sin ese perfil se mantiene el requisito HTTPS
|
`providers-mock` y puerto 8788. Sin ese perfil se mantiene el requisito HTTPS
|
||||||
normal; local/test conserva su excepción previa para loopback.
|
normal; local/test conserva su excepción previa para loopback.
|
||||||
|
|
@ -84,8 +85,10 @@ Antes de habilitar la demo, configurar en `.env` REQUEST_API_ENABLED=true,
|
||||||
WORKER_ENABLED=true y, para administrar flujos, FLOW_ADMIN_ENABLED=true y
|
WORKER_ENABLED=true y, para administrar flujos, FLOW_ADMIN_ENABLED=true y
|
||||||
ADMIN_BUSINESS_SCOPES. Se requieren el usuario DB de runtime restringido y las
|
ADMIN_BUSINESS_SCOPES. Se requieren el usuario DB de runtime restringido y las
|
||||||
credenciales separadas de migración descritos en [SECURITY.md](../../specs/SECURITY.md).
|
credenciales separadas de migración descritos en [SECURITY.md](../../specs/SECURITY.md).
|
||||||
Los JWT siguen procediendo de tu Keycloak. Este servicio no genera tokens JWT ni
|
El orquestador usa un token fijo en la POC, sin Keycloak. En Postman colocar
|
||||||
crea tipos, flujos, solicitudes o documentos automáticamente.
|
MOCK_ADMIN_TOKEN en accessToken, o el valor de ORCHESTRATOR_DEMO_TOKEN si lo
|
||||||
|
configuraste por separado. Ver [POC_AUTH.md](../../specs/POC_AUTH.md). No se crean
|
||||||
|
tipos, flujos, solicitudes o documentos automáticamente.
|
||||||
|
|
||||||
Aplicar en el entorno de demo:
|
Aplicar en el entorno de demo:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"info": {
|
"info": {
|
||||||
"name": "Orquestador NMP — server02",
|
"name": "Orquestador NMP — server02",
|
||||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||||
"description": "Ejecutar solicitudes manualmente por carpetas. No ejecutar toda la colección: publica y activa flujos, crea solicitudes y acepta eventos. Usar entorno de demo y datos sintéticos. Tokens vacíos: configurar en variables de colección. Los tokens específicos vacíos usan accessToken. Ver README.md adjunto. Las pruebas funcionales requieren flags, rol DB restringido, catálogo y proveedores configurados."
|
"description": "Ejecutar solicitudes manualmente por carpetas. No ejecutar toda la colección: publica y activa flujos, crea solicitudes y acepta eventos. Usar entorno de demo y datos sintéticos. Tokens vacíos: configurar en variables de colección. Los tokens específicos vacíos usan accessToken. Ver README.md adjunto. Las pruebas funcionales requieren flags, rol DB restringido, catálogo y proveedores configurados. POC sin Keycloak: con el perfil demo-token, accessToken es ORCHESTRATOR_DEMO_TOKEN (por defecto MOCK_ADMIN_TOKEN al usar compose.mock.yaml). Dejar los tokens específicos vacíos. Ver specs/POC_AUTH.md."
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"type": "bearer",
|
"type": "bearer",
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,15 @@ Publicar, activar, crear solicitudes y enviar eventos modifican datos.
|
||||||
|
|
||||||
1. Ejecutar la carpeta **01**: salud/readiness/liveness deben devolver 200 UP;
|
1. Ejecutar la carpeta **01**: salud/readiness/liveness deben devolver 200 UP;
|
||||||
la petición sin token debe devolver 401.
|
la petición sin token debe devolver 401.
|
||||||
2. En Variables de la colección, colocar un access token de Keycloak vigente en
|
2. Para la POC con `compose.mock.yaml`, colocar en `accessToken` el token fijo
|
||||||
`accessToken`, sin el prefijo Bearer. Guardarlo solo localmente; no compartir
|
`ORCHESTRATOR_DEMO_TOKEN` o, si no lo definiste, el valor de `MOCK_ADMIN_TOKEN`.
|
||||||
una exportación con tokens. Debe tener el issuer y audiencia configurados,
|
No anteponer Bearer. No requiere Keycloak. Ver [POC_AUTH](../../specs/POC_AUTH.md).
|
||||||
`sub`, expiración y el claim `business_scope` no vacío.
|
3. Dejar vacíos `editorToken`, `publisherToken`, `adminToken` y `operatorToken`
|
||||||
3. Para administración se pueden configurar `editorToken`, `publisherToken` y
|
para reutilizar accessToken. El servidor asigna usuario poc-user, ámbito demo
|
||||||
`adminToken` por separado. Si están vacíos se usa `accessToken`. Roles:
|
y roles de la POC. El ámbito debe estar en ADMIN_BUSINESS_SCOPES.
|
||||||
FLOW_EDITOR para crear/simular; FLOW_PUBLISHER para publicar; FLOW_ADMIN para
|
4. En modo normal sin demo-token, accessToken sí debe ser un JWT válido de Keycloak,
|
||||||
activar. El business_scope debe estar en ADMIN_BUSINESS_SCOPES.
|
con issuer/audiencia/roles/business_scope configurados. Guardar cualquier token
|
||||||
|
solo localmente y no compartir una exportación con secretos.
|
||||||
|
|
||||||
## Simulación sin llamadas a proveedores
|
## Simulación sin llamadas a proveedores
|
||||||
|
|
||||||
|
|
@ -34,7 +35,7 @@ Publicar, activar, crear solicitudes y enviar eventos modifican datos.
|
||||||
## Recorrido persistente
|
## Recorrido persistente
|
||||||
|
|
||||||
Solo en entorno de demo con proveedores mock configurados. La colección no
|
Solo en entorno de demo con proveedores mock configurados. La colección no
|
||||||
levanta mocks ni configura Keycloak, roles DB, contratos o tipos de solicitud.
|
levanta mocks ni configura roles DB, contratos o tipos de solicitud.
|
||||||
|
|
||||||
1. Habilitar REQUEST_API_ENABLED y WORKER_ENABLED con el runtime DB restringido.
|
1. Habilitar REQUEST_API_ENABLED y WORKER_ENABLED con el runtime DB restringido.
|
||||||
Configurar rutas HTTP de proveedores de demo. El tipo CREDITO debe existir;
|
Configurar rutas HTTP de proveedores de demo. El tipo CREDITO debe existir;
|
||||||
|
|
|
||||||
|
|
@ -532,3 +532,15 @@ Límites: no proveedor real, almacenamiento documental ni antivirus; metadatos m
|
||||||
El usuario solicita evitar certificados para la POC; sustituye la decisión TLS del mock en ADR-021. MockServer, healthcheck, rutas y Postman pasan a HTTP. Se eliminan generación de certificados, mounts de claves/truststore y JAVA_TOOL_OPTIONS del override; las respuestas persistidas se mantienen y no se borran volúmenes anteriores.
|
El usuario solicita evitar certificados para la POC; sustituye la decisión TLS del mock en ADR-021. MockServer, healthcheck, rutas y Postman pasan a HTTP. Se eliminan generación de certificados, mounts de claves/truststore y JAVA_TOOL_OPTIONS del override; las respuestas persistidas se mantienen y no se borran volúmenes anteriores.
|
||||||
|
|
||||||
El override activa el perfil explícito demo-mock junto al perfil DB. HttpRoutes permite HTTP hacia providers-mock:8788 únicamente con ese perfil; el requisito HTTPS normal y la excepción local/test para loopback se conservan. Se mantiene el token de administración del mock y la seguridad JWT del orquestador. No se habilita HTTP a otros hosts ni puertos por el perfil demo. La comunicación de la POC no está cifrada. No se modifica idempotencia ni versiones. Validación en E30; Docker sigue pendiente.
|
El override activa el perfil explícito demo-mock junto al perfil DB. HttpRoutes permite HTTP hacia providers-mock:8788 únicamente con ese perfil; el requisito HTTPS normal y la excepción local/test para loopback se conservan. Se mantiene el token de administración del mock y la seguridad JWT del orquestador. No se habilita HTTP a otros hosts ni puertos por el perfil demo. La comunicación de la POC no está cifrada. No se modifica idempotencia ni versiones. Validación en E30; Docker sigue pendiente.
|
||||||
|
|
||||||
|
## ADR-023 — Token fijo para autenticación de la POC — 2026-10-30
|
||||||
|
|
||||||
|
El usuario solicita quitar la dependencia de Keycloak en la POC y usar un token similar al mock. El perfil explícito demo-token selecciona un decoder de secreto opaco: compara hashes SHA-256 en tiempo constante y construye internamente la identidad que esperan los controladores. No analiza ni firma un JWT, no consulta issuer/JWKS y no acepta roles/identidad del cliente. Token requerido de 32..256 caracteres base64url; ausencia o formato inválido impide el arranque.
|
||||||
|
|
||||||
|
La identidad configurada por el servidor es poc-user/demo por defecto, con ANALISTA, FLOW_EDITOR, FLOW_PUBLISHER, FLOW_ADMIN y EXECUTION_OPERATOR. Se conservan la cadena de autorización, ownership/scope, permisos DB, flags funcionales e idempotencia. Todas las llamadas con ese secreto comparten identidad. El secreto no se retiene en el principal, no se exporta en Postman y no se imprime. No tiene expiración automática; rotación mediante configuración y recreación.
|
||||||
|
|
||||||
|
Alternativas descartadas: permitir todas las rutas elimina permisos y trazabilidad; JWT autofirmado mantiene infraestructura innecesaria para esta solicitud. Se reutiliza la representación Jwt interna exclusivamente como adaptación a los controladores existentes, documentada como identidad sintética, nunca como token JWT emitido.
|
||||||
|
|
||||||
|
compose.mock.yaml activa demo-token junto a demo-mock y el perfil DB; ORCHESTRATOR_DEMO_TOKEN permite secreto separado y por defecto reutiliza MOCK_ADMIN_TOKEN. Subject/ámbito configurables; ADMIN_BUSINESS_SCOPES conserva valores explícitos o usa ámbito demo. Variables JWT dejan de ser obligatorias durante interpolación de Compose para permitir esta combinación; la aplicación normal exige issuer/JWKS/audiencia no vacíos. Sin demo-token se mantiene Nimbus RS256 y el token fijo no se acepta aunque esté configurado.
|
||||||
|
|
||||||
|
Validación: pruebas HTTP aisladas sin DB/JWKS y pruebas de endpoints persistentes con PostgreSQL de pruebas; evidencia E31. Esta excepción de POC no cambia la configuración de producción ni cierra T16/T17. HTTP sin cifrado sigue siendo la modalidad de demo elegida por el usuario.
|
||||||
|
|
|
||||||
|
|
@ -278,3 +278,5 @@ Seguimiento T16 (2026-10-28, E26): corregidas las entradas del build Docker para
|
||||||
Extensión autorizada 2026-10-29: servicio de proveedores mock configurable manualmente (ADR-021), Compose opcional y colección Postman. Entregable relacionado con demos de T16/T17; no adelanta el cierre de esas tareas. Criterios: configuración HTTP de status/cuerpo/demora, persistencia tras reinicio, HTTPS y token administrativo verificados; despliegue Docker separado de la evidencia local. Ver E29 en PROGRESS.
|
Extensión autorizada 2026-10-29: servicio de proveedores mock configurable manualmente (ADR-021), Compose opcional y colección Postman. Entregable relacionado con demos de T16/T17; no adelanta el cierre de esas tareas. Criterios: configuración HTTP de status/cuerpo/demora, persistencia tras reinicio, HTTPS y token administrativo verificados; despliegue Docker separado de la evidencia local. Ver E29 en PROGRESS.
|
||||||
|
|
||||||
Actualización 2026-10-29 (ADR-022): por solicitud del usuario, el mock de la POC usa HTTP sin certificados. Perfil demo-mock limita la excepción a providers-mock:8788. Sustituye el criterio HTTPS de la extensión anterior; se conservan configuración, persistencia y token administrativo. T16/T17 mantienen su estado.
|
Actualización 2026-10-29 (ADR-022): por solicitud del usuario, el mock de la POC usa HTTP sin certificados. Perfil demo-mock limita la excepción a providers-mock:8788. Sustituye el criterio HTTPS de la extensión anterior; se conservan configuración, persistencia y token administrativo. T16/T17 mantienen su estado.
|
||||||
|
|
||||||
|
Extensión autorizada 2026-10-30 (ADR-023): autenticación de POC con token fijo y perfil demo-token, sin Keycloak. Criterios: fail-closed sin token correcto, identidad/roles del servidor, ACL de propiedad/ámbito, persistencia/idempotencia y regresión JWT normal. No cierra T16/T17; ver POC_AUTH.md y evidencia E31.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
# Autenticación de la POC sin Keycloak
|
||||||
|
|
||||||
|
El perfil explícito `demo-token` usa un token fijo en `Authorization: Bearer ...`.
|
||||||
|
No necesita un JWT, issuer, audiencia ni JWKS de Keycloak. El resto de las rutas
|
||||||
|
sigue protegido; sin token o con un valor incorrecto responde 401.
|
||||||
|
|
||||||
|
El override `compose.mock.yaml` activa `postgres96,demo-mock,demo-token` por defecto.
|
||||||
|
Para usarlo basta el `MOCK_ADMIN_TOKEN` existente (al menos 32 caracteres base64url:
|
||||||
|
letras, números, guion o guion bajo). Compose lo reutiliza como token del
|
||||||
|
orquestador si no defines `ORCHESTRATOR_DEMO_TOKEN`.
|
||||||
|
|
||||||
|
Opcionalmente, separar tokens en `.env`:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
# Sustituir por un token aleatorio de 32 a 256 caracteres base64url.
|
||||||
|
ORCHESTRATOR_DEMO_TOKEN=
|
||||||
|
ORCHESTRATOR_DEMO_SUBJECT=poc-user
|
||||||
|
ORCHESTRATOR_DEMO_SCOPE=demo
|
||||||
|
ADMIN_BUSINESS_SCOPES=demo
|
||||||
|
```
|
||||||
|
|
||||||
|
Un valor vacío de ORCHESTRATOR_DEMO_TOKEN en Compose usa MOCK_ADMIN_TOKEN. Al
|
||||||
|
arrancar directamente con Java y demo-token, ORCHESTRATOR_DEMO_TOKEN es obligatorio.
|
||||||
|
No guardar tokens reales en el repositorio ni en las colecciones exportadas.
|
||||||
|
|
||||||
|
Aplicar la configuración y reconstruir el servicio:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f compose.yaml -f compose.mock.yaml up -d --build orquestador
|
||||||
|
docker compose -f compose.yaml -f compose.mock.yaml exec orquestador printenv SPRING_PROFILES_ACTIVE
|
||||||
|
```
|
||||||
|
|
||||||
|
En la colección Postman **Orquestador NMP**, establecer `accessToken` con ese
|
||||||
|
token fijo, sin escribir Bearer. Dejar vacíos `editorToken`, `publisherToken`,
|
||||||
|
`adminToken` y `operatorToken` para que reutilicen accessToken. Borrar valores
|
||||||
|
antiguos de esos campos y de variables de entorno de Postman que los sobrescriban.
|
||||||
|
La colección del mock mantiene `mockAdminToken=MOCK_ADMIN_TOKEN`.
|
||||||
|
|
||||||
|
## Identidad y permisos
|
||||||
|
|
||||||
|
El servidor asocia el token a `poc-user`, ámbito `demo`, y roles ANALISTA,
|
||||||
|
FLOW_EDITOR, FLOW_PUBLISHER, FLOW_ADMIN y EXECUTION_OPERATOR. Subject y ámbito
|
||||||
|
se pueden configurar con las variables indicadas; roles fijos para la demo.
|
||||||
|
El cliente no puede escoger identidad/roles mediante el cuerpo o cabeceras.
|
||||||
|
|
||||||
|
Todas las personas que usen el mismo token son el mismo usuario de POC. Las
|
||||||
|
solicitudes creadas con otro subject o ámbito (incluido el usuario previo de
|
||||||
|
Keycloak) no se vuelven accesibles automáticamente. Cambiar el token conservando
|
||||||
|
subject y ámbito permite seguir trabajando con las solicitudes de la POC.
|
||||||
|
|
||||||
|
Se conservan las comprobaciones de propiedad, ámbito, roles, If-Match,
|
||||||
|
idempotencia y el rol restringido de PostgreSQL. La administración exige que
|
||||||
|
ADMIN_BUSINESS_SCOPES incluya el ámbito configurado. Un valor previo en `.env`
|
||||||
|
no se reemplaza automáticamente por demo.
|
||||||
|
|
||||||
|
FLOW_ADMIN_ENABLED, REQUEST_API_ENABLED y WORKER_ENABLED siguen siendo
|
||||||
|
interruptores independientes. Para el recorrido completo habilitarlos y usar
|
||||||
|
credenciales DB de runtime restringidas y migración separadas según SECURITY.md.
|
||||||
|
El token no crea tipos, flujos ni solicitudes y no evita esa comprobación DB.
|
||||||
|
|
||||||
|
## Modo normal
|
||||||
|
|
||||||
|
Sin `demo-token`, continúa la validación RS256, issuer, audiencia, expiración y
|
||||||
|
claims del JWT. JWT_ISSUER_URI, JWT_JWK_SET_URI y JWT_AUDIENCE son obligatorios
|
||||||
|
al arrancar la aplicación en modo normal, aunque Compose permita valores vacíos
|
||||||
|
para poder combinarlo con el override de POC. Configurar un token fijo sin
|
||||||
|
activar el perfil no habilita este modo.
|
||||||
|
|
||||||
|
Internamente se adapta la identidad a la representación usada por los
|
||||||
|
controladores existentes. No se firma un JWT ni se confía en claims proporcionados
|
||||||
|
por el cliente; únicamente se compara el secreto configurado. No hay caducidad
|
||||||
|
automática del token fijo: rotarlo requiere recrear el servicio. Esta POC usa
|
||||||
|
HTTP sin cifrado conforme a la configuración de demo solicitada.
|
||||||
|
|
@ -469,3 +469,12 @@ Con la configuración del repositorio, readiness incluye PostgreSQL. Esta prueba
|
||||||
- `mvnw.cmd --batch-mode --no-transfer-progress test -Dtest=HttpIntegrationTest`: BUILD SUCCESS, 24 pruebas, cero fallos/errores/omisiones, 40.400 s (17:56:56 -06:00). Incluye rechazo del host mock sin habilitación, aceptación explícita y rechazo de otros hosts/puertos. Log target/mock-http-maven.log.
|
- `mvnw.cmd --batch-mode --no-transfer-progress test -Dtest=HttpIntegrationTest`: BUILD SUCCESS, 24 pruebas, cero fallos/errores/omisiones, 40.400 s (17:56:56 -06:00). Incluye rechazo del host mock sin habilitación, aceptación explícita y rechazo de otros hosts/puertos. Log target/mock-http-maven.log.
|
||||||
- `python demo/providers-mock/test_mock.py` con JDK 21: 4 tests OK en 64.015 s; HTTP real, healthcheck Java, configuración, persistencia, errores/demora y metadatos. Parseo de Compose, rutas y colección Postman aprobado; sin mounts de certificados ni JAVA_TOOL_OPTIONS en override. git diff --check aprobado.
|
- `python demo/providers-mock/test_mock.py` con JDK 21: 4 tests OK en 64.015 s; HTTP real, healthcheck Java, configuración, persistencia, errores/demora y metadatos. Parseo de Compose, rutas y colección Postman aprobado; sin mounts de certificados ni JAVA_TOOL_OPTIONS en override. git diff --check aprobado.
|
||||||
- No se ejecutó Docker ni despliegue en server02. T16 sigue BLOCKED, T17 PENDING. Aplicar `docker compose -f compose.yaml -f compose.mock.yaml up -d --build` y reimportar colección del mock; la reconstrucción del orquestador es necesaria para la nueva excepción explícita.
|
- No se ejecutó Docker ni despliegue en server02. T16 sigue BLOCKED, T17 PENDING. Aplicar `docker compose -f compose.yaml -f compose.mock.yaml up -d --build` y reimportar colección del mock; la reconstrucción del orquestador es necesaria para la nueva excepción explícita.
|
||||||
|
|
||||||
|
## E31 — Autenticación de POC sin Keycloak — 2026-10-30
|
||||||
|
|
||||||
|
- Pedido explícito: token fijo similar al mock en lugar de integrar Keycloak. Perfil demo-token con DemoTokenDecoder, identidad/roles server-side y comparación de hashes en tiempo constante. Token incorrecto devuelve 401; configuración inválida falla al arrancar. Se mantienen ACL, flags, idempotencia y rol DB restringido. Sin perfil, Nimbus RS256 sigue siendo el decoder. ADR-023 y POC_AUTH.md.
|
||||||
|
- Compose opcional activa demo-token y reutiliza MOCK_ADMIN_TOKEN salvo ORCHESTRATOR_DEMO_TOKEN explícito. Subject poc-user y ámbito demo por defecto. Compose base permite interpolar JWT vacío para la POC, pero el arranque normal exige issuer/JWKS/audiencia. Colección Postman mantiene Bearer/accessToken, sin secretos guardados.
|
||||||
|
- Verificación dirigida: `mvnw.cmd --batch-mode --no-transfer-progress test -Dtest=DemoTokenSecurityTest,DemoTokenHttpTest,SecurityPolicyTest`: BUILD SUCCESS, 12 pruebas, cero fallos/errores/omisiones, 21.190 s. HTTP real sin DB/JWKS: autenticación, identidad no falsificable, propiedad/ámbito y rechazo del secreto en modo normal. El primer intento coincidió con reubicación del controlador de test; corregido al completar su aislamiento del escaneo productivo y repetir.
|
||||||
|
- Verificación final: `mvnw.cmd --batch-mode --no-transfer-progress verify -Pmigration-tests-external -Dit.test=DemoTokenIT,SecurityHttpIT`, JDK 21, exclusivamente orquestador_test con schemas/roles temporales. BUILD SUCCESS, 1:40 min, finalizado 2026-10-30T02:52:49-06:00. 332 unitarias + 7 IT = 339 aprobadas, sin fallos/errores/omisiones. Regresión JWT normal (6 IT) y POC con JWKS detenido (1 IT): crear/publicar/activar flujo, crear/repetir solicitud, identidad persistida, aceptar/repetir evento e historial. No ejecuta worker el nuevo IT.
|
||||||
|
- Evidencia: target/demo-token-tests.log, target/demo-token-verify.log y reportes Surefire/Failsafe. JAR SHA256 7B0BA4D26106248D762F1C4462D416DC5E3BDEAC509DFDD45039D40D117F57F0. JAR incluye perfil y excluye clases de prueba. YAML, Postman sin secretos y git diff --check aprobados.
|
||||||
|
- No hubo despliegue ni cambios sobre orquestador/server02:8787. Docker/Compose real y suite integral Testcontainers siguen pendientes: T16 BLOCKED, T17 PENDING. Aplicar reconstrucción con ambos Compose y poner token fijo en accessToken, dejando tokens específicos vacíos.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
# Seguridad T07
|
# Seguridad T07
|
||||||
|
|
||||||
|
Para la POC sin Keycloak, el perfil explícito `demo-token` permite un secreto fijo
|
||||||
|
según [POC_AUTH.md](POC_AUTH.md) y ADR-023. La validación JWT de esta guía describe
|
||||||
|
el modo normal, activo cuando no se selecciona ese perfil.
|
||||||
|
|
||||||
El servicio valida tokens RS256 mediante JWKS, emisor exacto, audiencia configurada,
|
El servicio valida tokens RS256 mediante JWKS, emisor exacto, audiencia configurada,
|
||||||
expiración obligatoria y sujeto no vacío. También valida nbf cuando está presente;
|
expiración obligatoria y sujeto no vacío. También valida nbf cuando está presente;
|
||||||
el validador temporal de Spring admite 60 segundos de desfase. Las pruebas usan
|
el validador temporal de Spring admite 60 segundos de desfase. Las pruebas usan
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.organizacion.orchestrator.shared.infrastructure.security;
|
||||||
|
|
||||||
|
import com.organizacion.orchestrator.shared.domain.Actor;
|
||||||
|
import org.springframework.security.oauth2.jwt.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.*;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/** Explicit demo-token profile only. Adapts a fixed secret to the existing principal/ACL contract.
|
||||||
|
* No JWT is parsed, signed or trusted here; identity and roles come exclusively from server configuration.
|
||||||
|
*/
|
||||||
|
public final class DemoTokenDecoder implements JwtDecoder {
|
||||||
|
private static final Set<String> ROLES=Set.of("ANALISTA","FLOW_EDITOR","FLOW_PUBLISHER","FLOW_ADMIN","EXECUTION_OPERATOR");
|
||||||
|
private final byte[] expected;
|
||||||
|
private final Actor actor;
|
||||||
|
private final String scopeClaim;
|
||||||
|
public DemoTokenDecoder(String token,String subject,String scope,String scopeClaim){
|
||||||
|
if(token==null||!token.matches("[A-Za-z0-9_-]{32,256}"))
|
||||||
|
throw new IllegalArgumentException("Demo token must contain 32..256 base64url characters");
|
||||||
|
if(scopeClaim==null||scopeClaim.isBlank()||Set.of("sub","iat","exp","aud","iss","realm_access","resource_access").contains(scopeClaim))
|
||||||
|
throw new IllegalArgumentException("Invalid demo scope claim");
|
||||||
|
this.actor=new Actor(subject,scope,ROLES);this.scopeClaim=scopeClaim;this.expected=digest(token);
|
||||||
|
}
|
||||||
|
private static byte[] digest(String value){
|
||||||
|
try{return MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8));}
|
||||||
|
catch(NoSuchAlgorithmException e){throw new IllegalStateException(e);}
|
||||||
|
}
|
||||||
|
@Override public Jwt decode(String token)throws JwtException{
|
||||||
|
if(token==null||token.length()>256||!MessageDigest.isEqual(expected,digest(token)))
|
||||||
|
throw new BadJwtException("Invalid demo token");
|
||||||
|
Instant now=Instant.now();
|
||||||
|
// Synthetic internal principal, not a JWT for clients. Never retain the shared secret as tokenValue.
|
||||||
|
return Jwt.withTokenValue("demo-authenticated").header("alg","DEMO")
|
||||||
|
.subject(actor.subject()).issuedAt(now).expiresAt(now.plusSeconds(60))
|
||||||
|
.claim(scopeClaim,actor.businessScope()).claim("realm_access",Map.of("roles",List.copyOf(actor.roles())))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ import org.springframework.security.oauth2.core.*;
|
||||||
import org.springframework.security.oauth2.jwt.Jwt;
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/** Converts only verified JWT claims. OAuth scopes and body fields cannot grant technical roles. */
|
/** Maps authenticated claims: verified JWT or server-built demo identity. Body fields cannot grant roles. */
|
||||||
public final class JwtActorMapper {
|
public final class JwtActorMapper {
|
||||||
private final String businessScopeClaim,clientId;
|
private final String businessScopeClaim,clientId;
|
||||||
public JwtActorMapper(String businessScopeClaim,String clientId){
|
public JwtActorMapper(String businessScopeClaim,String clientId){
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.organizacion.orchestrator.shared.infrastructure.security;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||||
import org.springframework.security.authorization.AuthorizationDecision;
|
import org.springframework.security.authorization.AuthorizationDecision;
|
||||||
|
|
@ -26,9 +27,12 @@ public class SecurityConfiguration {
|
||||||
return new JwtActorMapper(scope,audience);
|
return new JwtActorMapper(scope,audience);
|
||||||
}
|
}
|
||||||
@Bean
|
@Bean
|
||||||
|
@Profile("!demo-token")
|
||||||
JwtDecoder jwtDecoder(@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") String issuer,
|
JwtDecoder jwtDecoder(@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") String issuer,
|
||||||
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") String jwks,
|
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") String jwks,
|
||||||
@Value("${spring.security.oauth2.resourceserver.jwt.audiences}") String audience){
|
@Value("${spring.security.oauth2.resourceserver.jwt.audiences}") String audience){
|
||||||
|
if(issuer.isBlank()||jwks.isBlank()||audience.isBlank())
|
||||||
|
throw new IllegalArgumentException("JWT issuer, JWKS and audience are required outside demo-token");
|
||||||
var decoder=NimbusJwtDecoder.withJwkSetUri(jwks).jwsAlgorithm(SignatureAlgorithm.RS256).build();
|
var decoder=NimbusJwtDecoder.withJwkSetUri(jwks).jwsAlgorithm(SignatureAlgorithm.RS256).build();
|
||||||
OAuth2TokenValidator<Jwt> required=jwt->{
|
OAuth2TokenValidator<Jwt> required=jwt->{
|
||||||
if(jwt.getExpiresAt()==null||jwt.getSubject()==null||jwt.getSubject().isBlank()
|
if(jwt.getExpiresAt()==null||jwt.getSubject()==null||jwt.getSubject().isBlank()
|
||||||
|
|
@ -40,6 +44,14 @@ public class SecurityConfiguration {
|
||||||
return decoder;
|
return decoder;
|
||||||
}
|
}
|
||||||
@Bean
|
@Bean
|
||||||
|
@Profile("demo-token")
|
||||||
|
JwtDecoder demoTokenDecoder(@Value("${orchestrator.security.demo.token:}")String token,
|
||||||
|
@Value("${orchestrator.security.demo.subject:poc-user}")String subject,
|
||||||
|
@Value("${orchestrator.security.demo.scope:demo}")String scope,
|
||||||
|
@Value("${orchestrator.security.business-scope-claim:business_scope}")String scopeClaim){
|
||||||
|
return new DemoTokenDecoder(token,subject,scope,scopeClaim);
|
||||||
|
}
|
||||||
|
@Bean
|
||||||
SecurityFilterChain securityFilterChain(HttpSecurity http,JwtActorMapper actors,
|
SecurityFilterChain securityFilterChain(HttpSecurity http,JwtActorMapper actors,
|
||||||
@Value("${orchestrator.admin.enabled:false}") boolean enabled,
|
@Value("${orchestrator.admin.enabled:false}") boolean enabled,
|
||||||
@Value("${orchestrator.admin.business-scopes:}") String scopes) throws Exception {
|
@Value("${orchestrator.admin.business-scopes:}") String scopes) throws Exception {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# POC only: no issuer discovery or JWKS requests. Normal profiles retain RS256 validation.
|
||||||
|
spring:
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
resourceserver:
|
||||||
|
jwt:
|
||||||
|
issuer-uri: ""
|
||||||
|
jwk-set-uri: ""
|
||||||
|
audiences: orchestrator-demo
|
||||||
|
orchestrator:
|
||||||
|
security:
|
||||||
|
business-scope-claim: business_scope
|
||||||
|
demo:
|
||||||
|
token: ${ORCHESTRATOR_DEMO_TOKEN:}
|
||||||
|
subject: ${ORCHESTRATOR_DEMO_SUBJECT:poc-user}
|
||||||
|
scope: ${ORCHESTRATOR_DEMO_SCOPE:demo}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.organizacion.demosecuritytest;
|
||||||
|
|
||||||
|
import com.organizacion.orchestrator.shared.infrastructure.security.*;
|
||||||
|
import com.organizacion.orchestrator.shared.application.ResourceAccess;
|
||||||
|
import com.organizacion.orchestrator.shared.application.port.ResourceOwnershipStore;
|
||||||
|
import com.organizacion.orchestrator.shared.domain.*;
|
||||||
|
import com.organizacion.orchestrator.shared.domain.Identifiers.RequestId;
|
||||||
|
import org.junit.jupiter.api.*;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.web.server.context.WebServerApplicationContext;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
import org.springframework.context.annotation.*;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.security.oauth2.jwt.*;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import java.net.*;
|
||||||
|
import java.net.http.*;
|
||||||
|
import java.util.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/** Real HTTP authentication without a database, issuer, JWKS server or signed JWT.
|
||||||
|
* Test-only controller probes the same principal and ownership gate used by business endpoints.
|
||||||
|
*/
|
||||||
|
class DemoTokenHttpTest {
|
||||||
|
static final String TOKEN="synthetic-demo-token-0123456789abcdef";
|
||||||
|
static ConfigurableApplicationContext app;
|
||||||
|
static String base;
|
||||||
|
static final HttpClient HTTP=HttpClient.newHttpClient();
|
||||||
|
@Configuration(proxyBeanMethods=false)
|
||||||
|
@EnableAutoConfiguration(excludeName={"org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration",
|
||||||
|
"org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration",
|
||||||
|
"org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration"})
|
||||||
|
@Import({SecurityConfiguration.class,Probe.class})
|
||||||
|
static class TestApp {}
|
||||||
|
@RestController
|
||||||
|
static class Probe {
|
||||||
|
final JwtActorMapper mapper;
|
||||||
|
Probe(JwtActorMapper mapper){this.mapper=mapper;}
|
||||||
|
@GetMapping("/probe") Map<String,Object> identity(@AuthenticationPrincipal Jwt principal){
|
||||||
|
var actor=mapper.actor(principal);
|
||||||
|
return Map.of("subject",actor.subject(),"scope",actor.businessScope(),"roles",actor.roles());
|
||||||
|
}
|
||||||
|
@PostMapping("/api/v1/admin/flows") String admin(){return "authorized";}
|
||||||
|
@GetMapping("/ownership/{subject}/{scope}") boolean ownership(@AuthenticationPrincipal Jwt principal,@PathVariable String subject,@PathVariable String scope){
|
||||||
|
var owner=new ResourceOwner(new RequestId(UUID.randomUUID()),subject,scope);
|
||||||
|
var gate=new ResourceAccess((kind,id)->Optional.of(owner));
|
||||||
|
try{gate.require(mapper.actor(principal),ResourceOwnershipStore.Kind.REQUEST,owner.requestId().value());return true;}
|
||||||
|
catch(DomainException denied){return false;}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void start(boolean demo,String scopes){
|
||||||
|
var args=new ArrayList<>(List.of("--server.address=127.0.0.1","--server.port=0","--logging.level.root=ERROR",
|
||||||
|
"--management.endpoint.health.validate-group-membership=false","--orchestrator.admin.enabled=true",
|
||||||
|
"--orchestrator.admin.business-scopes="+scopes,"--orchestrator.security.demo.token="+TOKEN));
|
||||||
|
if(demo)args.add("--spring.profiles.active=demo-token");
|
||||||
|
else args.addAll(List.of("--spring.profiles.active=default","--spring.security.oauth2.resourceserver.jwt.issuer-uri=https://issuer.invalid",
|
||||||
|
"--spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://issuer.invalid/jwks","--spring.security.oauth2.resourceserver.jwt.audiences=normal"));
|
||||||
|
app=new SpringApplication(TestApp.class).run(args.toArray(String[]::new));
|
||||||
|
base="http://127.0.0.1:"+((WebServerApplicationContext)app).getWebServer().getPort();
|
||||||
|
}
|
||||||
|
@AfterEach void close(){if(app!=null){app.close();app=null;}}
|
||||||
|
static HttpResponse<String> call(String path,String token)throws Exception{
|
||||||
|
var b=HttpRequest.newBuilder(URI.create(base+path)).timeout(java.time.Duration.ofSeconds(10));
|
||||||
|
if(token!=null)b.header("Authorization","Bearer "+token);
|
||||||
|
if(path.startsWith("/api/"))b.POST(HttpRequest.BodyPublishers.ofString("{\"subject\":\"forged\",\"businessScope\":\"other\"}"));
|
||||||
|
// These headers must never choose the authenticated identity.
|
||||||
|
b.header("X-Subject","forged").header("X-Business-Scope","other");
|
||||||
|
return HTTP.send(b.build(),HttpResponse.BodyHandlers.ofString());
|
||||||
|
}
|
||||||
|
@Test void demoAuthenticatesOpaqueTokenAndKeepsOwnershipChecks()throws Exception{
|
||||||
|
start(true,"demo");
|
||||||
|
assertInstanceOf(DemoTokenDecoder.class,app.getBean(JwtDecoder.class));
|
||||||
|
assertEquals(200,call("/actuator/health",null).statusCode());
|
||||||
|
assertEquals(401,call("/probe",null).statusCode());
|
||||||
|
assertEquals(401,call("/probe","wrong").statusCode());
|
||||||
|
var response=call("/probe",TOKEN);
|
||||||
|
assertEquals(200,response.statusCode(),response.body());
|
||||||
|
assertTrue(response.body().contains("poc-user"));assertTrue(response.body().contains("ANALISTA"));
|
||||||
|
assertFalse(response.body().contains("forged"));assertFalse(response.body().contains(TOKEN));
|
||||||
|
assertEquals(200,call("/api/v1/admin/flows",TOKEN).statusCode());
|
||||||
|
assertEquals("true",call("/ownership/poc-user/demo",TOKEN).body());
|
||||||
|
assertEquals("false",call("/ownership/other/demo",TOKEN).body());
|
||||||
|
assertEquals("false",call("/ownership/poc-user/other",TOKEN).body());
|
||||||
|
}
|
||||||
|
@Test void adminScopeIsNotBypassedByDemoToken()throws Exception{
|
||||||
|
start(true,"other");
|
||||||
|
assertEquals(403,call("/api/v1/admin/flows",TOKEN).statusCode());
|
||||||
|
}
|
||||||
|
@Test void normalModeRejectsFixedTokenEvenWhenSecretIsConfigured()throws Exception{
|
||||||
|
start(false,"demo");
|
||||||
|
assertInstanceOf(NimbusJwtDecoder.class,app.getBean(JwtDecoder.class));
|
||||||
|
assertEquals(401,call("/probe",TOKEN).statusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.organizacion.orchestrator;
|
||||||
|
|
||||||
|
import com.organizacion.orchestrator.shared.infrastructure.security.DemoTokenDecoder;
|
||||||
|
import org.junit.jupiter.api.*;
|
||||||
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.*;
|
||||||
|
import java.util.UUID;
|
||||||
|
import static com.organizacion.orchestrator.SecurityHttpIT.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/** Real controllers and isolated PostgreSQL, fixed POC token, JWKS deliberately stopped. */
|
||||||
|
class DemoTokenIT {
|
||||||
|
static final String TOKEN=DemoTokenSecurityTest.TOKEN;
|
||||||
|
@BeforeAll static void setup()throws Exception{
|
||||||
|
SecurityHttpIT.start(true,true,"--spring.profiles.active=postgres96,demo-token",
|
||||||
|
"--orchestrator.security.demo.token="+TOKEN,"--orchestrator.security.demo.scope=scope-a");
|
||||||
|
assertInstanceOf(DemoTokenDecoder.class,app.getBean(JwtDecoder.class));
|
||||||
|
keys.stop(0);
|
||||||
|
}
|
||||||
|
@AfterAll static void cleanup(){SecurityHttpIT.stop();}
|
||||||
|
@Test void createPublishActivateAndAcceptEventWithFixedIdentityAndIdempotency()throws Exception{
|
||||||
|
assertEquals(401,call("POST","/api/v1/admin/flows","wrong",draft(),null).statusCode());
|
||||||
|
var created=call("POST","/api/v1/admin/flows",TOKEN,draft(),null);
|
||||||
|
assertEquals(201,created.statusCode(),created.body());
|
||||||
|
String id=json.readTree(created.body()).get("id").asText();
|
||||||
|
assertEquals(200,call("POST","/api/v1/admin/flows/"+id+"/publish",TOKEN,null,"\"0\"").statusCode());
|
||||||
|
String type="POC_"+UUID.randomUUID().toString().replace("-","");
|
||||||
|
admin.update("INSERT INTO "+schema+".request_type(type_key) VALUES (?)",type);
|
||||||
|
var active=call("PUT","/api/v1/admin/request-types/"+type+"/active-flow",TOKEN,"{\"flowId\":\""+id+"\"}","\"0\"");
|
||||||
|
assertEquals(200,active.statusCode(),active.body());
|
||||||
|
String key=UUID.randomUUID().toString();
|
||||||
|
var request=RequestCreationIT.post(type,key,"{}",TOKEN);
|
||||||
|
assertEquals(201,request.statusCode(),request.body());
|
||||||
|
assertEquals(request.body(),RequestCreationIT.post(type,key,"{}",TOKEN).body());
|
||||||
|
String requestId=json.readTree(request.body()).get("id").asText();
|
||||||
|
var persisted=admin.queryForMap("SELECT owner_subject,business_scope FROM "+schema+".request_instance WHERE id=?",UUID.fromString(requestId));
|
||||||
|
assertEquals("poc-user",persisted.get("owner_subject"));assertEquals("scope-a",persisted.get("business_scope"));
|
||||||
|
assertEquals(200,call("GET","/api/v1/requests/"+requestId,TOKEN,null,null).statusCode());
|
||||||
|
var event=HttpRequest.newBuilder(URI.create(base+"/api/v1/requests/"+requestId+"/events"))
|
||||||
|
.timeout(java.time.Duration.ofSeconds(20)).header("Authorization","Bearer "+TOKEN)
|
||||||
|
.header("Idempotency-Key",UUID.randomUUID().toString()).header("If-Match","\"0\"")
|
||||||
|
.header("Content-Type","application/json").POST(HttpRequest.BodyPublishers.ofString("{\"event\":\"SUBMIT\",\"data\":{}}"));
|
||||||
|
var accepted=http.send(event.build(),HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertEquals(202,accepted.statusCode(),accepted.body());
|
||||||
|
var replay=http.send(event.build(),HttpResponse.BodyHandlers.ofString());
|
||||||
|
assertEquals(202,replay.statusCode());assertEquals(accepted.body(),replay.body());
|
||||||
|
assertEquals(200,call("GET","/api/v1/requests/"+requestId+"/history",TOKEN,null,null).statusCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.organizacion.orchestrator;
|
||||||
|
|
||||||
|
import com.organizacion.orchestrator.shared.infrastructure.security.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.security.oauth2.jwt.*;
|
||||||
|
import java.util.Set;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class DemoTokenSecurityTest {
|
||||||
|
static final String TOKEN="synthetic-demo-token-0123456789abcdef";
|
||||||
|
@Test void fixedSecretProducesOnlyServerConfiguredIdentity(){
|
||||||
|
var decoder=new DemoTokenDecoder(TOKEN,"poc-user","demo","business_scope");
|
||||||
|
var jwt=decoder.decode(TOKEN);
|
||||||
|
var actor=new JwtActorMapper("business_scope","unused").actor(jwt);
|
||||||
|
assertEquals("poc-user",actor.subject());assertEquals("demo",actor.businessScope());
|
||||||
|
assertEquals(Set.of("ANALISTA","FLOW_EDITOR","FLOW_PUBLISHER","FLOW_ADMIN","EXECUTION_OPERATOR"),actor.roles());
|
||||||
|
assertNotEquals(TOKEN,jwt.getTokenValue());
|
||||||
|
for(String wrong:new String[]{"",TOKEN+"x","eyJhbGciOiJub25lIn0.e30.","wrong"})
|
||||||
|
assertThrows(BadJwtException.class,()->decoder.decode(wrong));
|
||||||
|
assertThrows(BadJwtException.class,()->decoder.decode(null));
|
||||||
|
}
|
||||||
|
@Test void missingOrInvalidConfigurationFailsClosedWithoutEchoingSecret(){
|
||||||
|
for(String bad:new String[]{"","short"," contains spaces and should never be printed in errors "}){
|
||||||
|
var error=assertThrows(IllegalArgumentException.class,()->new DemoTokenDecoder(bad,"poc-user","demo","business_scope"));
|
||||||
|
if(!bad.isEmpty())assertFalse(error.getMessage().contains(bad));
|
||||||
|
}
|
||||||
|
assertThrows(IllegalArgumentException.class,()->new DemoTokenDecoder(TOKEN,"","demo","business_scope"));
|
||||||
|
assertThrows(IllegalArgumentException.class,()->new DemoTokenDecoder(TOKEN,"poc-user","","business_scope"));
|
||||||
|
assertThrows(IllegalArgumentException.class,()->new DemoTokenDecoder(TOKEN,"poc-user","demo","sub"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue