← Volver al índice
🔐 Acceso Condicional · Seguridad

Travel CA Exception

Gestión de excepciones de Acceso Condicional para empleados en desplazamiento internacional. Dos modos de operación: aprobación explícita con MSAL delegado y Protected Actions, o autoservicio con token daemon.

🌐 Puerto 8091
🐍 Python + Flask
🔐 MSAL + Claims Challenge
Toggle autoservicio
📋 Jira integration
En producción
8091
Puerto del servicio
CP1
Client capability — Protected Actions
4
Operaciones Graph por excepción
08:00
Scheduler diario de expiración (Madrid)

Qué es y por qué existe

La política de Acceso Condicional "Risky Countries" bloquea el acceso a recursos corporativos desde países considerados de riesgo. Cuando un empleado viaja a uno de esos países por motivos de trabajo, necesita una excepción temporal, controlada y completamente reversible.

🚫 El problema

La política principal Risky Countries - 01 - Default bloquea cualquier intento de autenticación desde la Named Location #04 - Risky Countries. Si un empleado viaja a uno de esos países, pierde acceso a correo, Teams, SharePoint y cualquier recurso protegido por Entra ID. No existe mecanismo nativo en la política para hacer excepciones puntuales sin modificar objetos compartidos por todo el tenant.

✅ La solución

Esta aplicación crea una excepción temporal de mínimo privilegio por usuario: añade al viajero al grupo de exclusiones de la política principal, y crea objetos CA específicos para su viaje (Named Location + política CA) que le permiten acceder únicamente desde su país de destino y únicamente hasta su fecha de vuelta. Dos modos: con aprobación (MSAL delegado + Protected Actions + ticket SIS en Jira) o autoservicio (token daemon + ejecución directa, sin ticket SIS). El toggle se cambia en la BD SQLite sin reiniciar el servicio.

🔒 Política principal

Risky Countries - 01 - Default

9d0adb2b-ca25-4278-80ec-c627b3611bf8

🌎 Named Location #04

#04 - Risky Countries — lista de países bloqueados

1f753369-b550-4df1-9811-60709007b8c6

👥 Grupo de excepciones

CA - Travel Exceptions — excludeGroups en la política principal

9ef82868-2d3f-4abd-96eb-cfbc3783addc

Ahorro y eficiencia operativa

Antes de esta herramienta el proceso era enteramente manual en el portal de Entra ID. Cada excepción requería al menos un sysadmin con permisos de Acceso Condicional, sin posibilidad de delegación al helpdesk.

Antes — proceso manual

~30 min / excepción

Acceso al portal de Entra ID → crear Named Location → esperar propagación → crear CA policy → añadir al grupo. Proceso inverso al volver. Solo ejecutable por sysadmins con permisos Policy.ReadWrite.ConditionalAccess. Sin trazabilidad automática ni recordatorio de desactivación.

  • ~20 min activación (portal + esperas de propagación)
  • ~10 min desactivación + reintentos por error 1178
  • Riesgo de olvidar desactivar → excepción activa indefinidamente
  • Comunicación manual helpdesk → sysadmin para cada solicitud
  • Sin registro centralizado de excepciones activas

Ahora — web app automatizada

~5 min / excepción

Con aprobación: Helpdesk rellena el formulario (2 min) → ticket Jira creado → admin abre enlace, MFA y confirma (3 min) → app ejecuta las 4 operaciones Graph. Autoservicio: Helpdesk rellena el formulario → app ejecuta directamente con token daemon, sin intervención de admin. Vencimiento detectado automáticamente por el scheduler.

  • Helpdesk puede iniciar solicitudes sin permisos Graph
  • Autoservicio: ejecución instantánea sin MFA admin
  • Desactivación automática vía scheduler diario (en autoservicio, la aplica directamente)
  • Dashboard centralizado con todas las excepciones activas y su estado
  • 0 riesgo de excepción olvidada activa
~25 min
ahorrados por excepción
~83%
reducción de tiempo
0
excepciones olvidadas activas
100%
delegable al helpdesk

Beneficios adicionales

Seguridad

El scheduler detecta automáticamente las excepciones vencidas y notifica para su desactivación. Antes, una excepción podía quedar activa indefinidamente si nadie recordaba desactivarla, ampliando la superficie de ataque del tenant.

Trazabilidad y auditoría

Cada excepción queda registrada en SQLite con policy_id, location_id, approved_by y timestamps. Jira recibe comentarios automáticos en cada evento (aprobación, desactivación, cancelación). La etiqueta Travel-CA-Exception facilita búsquedas futuras.

Delegación

El helpdesk puede crear y hacer seguimiento de solicitudes sin necesidad de permisos Policy.ReadWrite.ConditionalAccess ni acceso al portal de Entra ID. El rol Approver, reservado a sysadmins, es el único que ejecuta operaciones sobre Graph.

Escalabilidad

El coste operativo por excepción adicional es prácticamente nulo. Picos de solicitudes (periodos vacacionales, eventos internacionales) no generan carga proporcional en el equipo de sistemas.

Arquitectura de seguridad

Cinco componentes de Entra ID se coordinan para crear una excepción de mínimo privilegio: el usuario solo puede acceder desde su país de destino exacto, y solo durante el periodo aprobado.

Componente Identificador / Patrón de nombre Descripción Rol en la excepción
Política principal
Risky Countries - 01 - Default
9d0adb2b-ca25-4278… Bloquea acceso desde Named Location #04 a todos los usuarios del tenant Es la política que se evita mediante el grupo excludeGroups
Named Location #04
#04 - Risky Countries
1f753369-b550-4df1… Lista de países de riesgo. Origen bloqueado por la política principal Condición de ubicación de la política principal; permanente, no se modifica nunca
Grupo de excepciones
CA - Travel Exceptions
9ef82868-2d3f-4abd… Grupo configurado como excludeGroups en la política principal Los miembros de este grupo quedan excluidos del bloqueo general; la app los añade y elimina
Named Location temporal
Creada por la app
Travel | DisplayName | countries | hasta YYYY-MM-DD Contiene únicamente el país (o países) de destino del viajero Referenciada en la política CA temporal como única ubicación permitida
Política CA temporal
Creada por la app
Travel | UPN | countries | hasta YYYY-MM-DD Política específica para el usuario viajero; bloquea si el acceso no proviene de su Named Location de viaje Restringe al usuario a acceder únicamente desde su país de destino; eliminada al deshabilitar

💡 Por qué funciona la lógica

La excepción combina dos mecanismos complementarios. Primero, el usuario entra en el grupo excludeGroups de la política principal — esto elimina el bloqueo genérico por país de riesgo. Sin más cambios, el usuario podría acceder desde cualquier país, incluyendo otros de riesgo ajenos a su viaje. Para evitarlo, la política CA temporal añade una restricción positiva: el usuario solo puede autenticarse si su IP coincide con su Named Location de viaje. El resultado es acceso de mínimo privilegio: exactamente desde el país de destino, exactamente durante el periodo aprobado.

🗑 Limpieza al deshabilitar

Al deshabilitar una excepción (manual o automáticamente por el scheduler), la app ejecuta tres operaciones de borrado en Graph: elimina la política CA temporal (DELETE /policies/conditionalAccessPolicies/{id}), elimina la Named Location temporal (DELETE /identity/conditionalAccess/namedLocations/{id}, con reintentos para el error 1178) y elimina la membresía del usuario en el grupo de excepciones. El tenant vuelve exactamente al estado anterior sin rastro en objetos CA compartidos.

Flujo completo — solicitud y aprobación

La app está en el puerto 8091, desplegada con WinSW en el servidor sisapps y accesible vía App Proxy en https://travelcaapprover-idealistaa82505660.msappproxy.net.

Nueva solicitud (Helpdesk / Operador)
1
Helpdesk abre /request — rellena el formulario: UPN del viajero (autocompletar desde el grupo "Domain internal users", caché de 5 minutos), países de destino (selector multi-país con 250 códigos ISO), fecha de vuelta y referencia de incidencia Jira (incidencia_ref)
2
App crea la excepción en SQLite — estado pending, genera un UUID token de aprobación con expiración de 7 días
3
App llama a Jira — crea una tarea SIS con la URL de aprobación en la descripción, la enlaza con incidencia_ref (link tipo "Relates") y añade la etiqueta Travel-CA-Exception a ambos tickets
4
Helpdesk ve la página de confirmación — con enlaces a la incidencia original y a la tarea SIS recién creada. No necesita hacer nada más
Aprobación (Admin)
5
Admin abre la tarea SIS en Jira — hace clic en el enlace de aprobación de la descripción y llega a la página /approve/{token} de la app
6
Admin hace clic en "Ejecutar activación" — POST /auth/start → la app inicia un MSAL auth code flow con client_capabilities=["CP1"], redirigiendo al admin a Entra ID para autenticación delegada
7
Primer token recibido (sin claim acrs) — la app intenta las operaciones Graph sobre políticas CA y recibe 401 Unauthorized con un claims challenge codificado en el cuerpo JSON de la respuesta
8
App parsea el claims challenge — extrae el valor claims del JSON, lo almacena en sesión Flask y redirige al admin de vuelta a Entra ID con el parámetro claims= en la URL de autorización
9
Entra ID solicita MFA step-up — el admin completa la verificación adicional. El nuevo token incluye el claim acrs=c1, satisfaciendo el requisito de Protected Actions de la política CA #0002
10
/exec-approve ejecuta las 4 operaciones Graph — (1) crea Named Location temporal, (2) espera propagación con reintentos, (3) crea política CA temporal, (4) añade el usuario al grupo CA – Travel Exceptions
11
Excepción marcada como activa en SQLite — se almacenan policy_id, location_id y approved_by para poder revertirla después
12
Jira: comentarios de aprobaciónadd_approval_comment en la incidencia_ref del helpdesk Y en la tarea SIS, con tabla wiki: UPN, países, fecha fin, policy_id, location_id, approved_by
13
Página de resultado — muestra el resumen de la excepción activa con botón "Ver incidencia" que abre el ticket Jira del operador
Desactivación manual
1
Admin hace clic en "Deshabilitar" en el dashboard — POST /request-disable genera un disable token (expira en 14 días) y presenta la página de confirmación con detalle de la excepción
2
Mismo flujo de auth que la aprobación — MSAL auth code delegado, claims challenge, MFA step-up, nuevo token con acrs=c1
3
Graph ejecuta 3 operaciones de borrado — DELETE política CA temporal, DELETE Named Location temporal (con reintentos para el error 1178), remove usuario del grupo CA – Travel Exceptions
4
Excepción marcada como disabled en SQLite — se registra quién la desactivó y el timestamp exacto
5
Jira: add_disable_comment — comentario en la incidencia original del helpdesk Y en la tarea SIS de activación, con resumen de la desactivación

Scheduler — expiración automática

APScheduler ejecuta una comprobación diaria a las 08:00 (Europe/Madrid) para detectar excepciones vencidas. El comportamiento depende del toggle approval_required en la BD.

⚡ Modo autoservicio (approval_required = false)

El scheduler aplica las desactivaciones directamente sin intervención humana:

  1. Adquiere un token daemon vía SisScripts App Registration (250c7cb4, certificado)
  2. Por cada excepción vencida: llama a graph.disable() → marca disabled en SQLite → add_disable_comment solo en incidencia_ref (sin ticket SIS)
  3. Si una falla, las demás continúan — cada excepción se procesa independientemente

👤 Modo con aprobación (approval_required = true)

El scheduler no aplica las desactivaciones — notifica para que un admin las ejecute con su MFA:

  1. Genera un disable token por cada excepción (expiran en 14 días)
  2. Crea una única tarea SIS en Jira con tabla wiki: usuario | países | fecha vencida | enlace de desactivación
  3. Enlaza el ticket con cada tarea SIS de activación + etiqueta Travel-CA-Exception

👤 Trigger manual

El dashboard incluye un botón "Revisar vencidas" en la cabecera que llama a run_now() del scheduler. Útil para forzar la comprobación sin esperar al horario automático, por ejemplo tras un reinicio del servicio. Ejecuta el mismo flujo condicional según el toggle activo en ese momento.

"scheduler": { "enabled": true, "hour": 8, "minute": 0, "timezone": "Europe/Madrid" }

Integración Jira

Todos los eventos del ciclo de vida de una excepción generan actividad en Jira. La integración se puede deshabilitar globalmente con jira.enabled: false.

Operación Momento Qué hace Tickets afectados Modo
create_enable_ticket Al crear la solicitud Crea tarea SIS con URL de aprobación en descripción Nuevo SIS + link "Relates" a incidencia_ref + etiqueta en ambos Con aprobación
add_approval_comment Al aprobar la excepción Comentario wiki con tabla de detalles: UPN, países, fecha fin, policy_id, location_id, approved_by incidencia_ref + tarea SIS (con aprob.) / solo incidencia_ref (autoservicio) Ambos
add_disable_comment Al deshabilitar (manual o scheduler) Comentario con resumen de desactivación y quién la ejecutó (o [scheduler]) Con aprobación: incidencia_ref + tarea SIS / Autoservicio: solo incidencia_ref Ambos
add_cancel_comment Al cancelar una solicitud pendiente Comentario indicando que la solicitud fue cancelada sin llegar a activarse Solo incidencia_ref del helpdesk Ambos
create_expiry_ticket Scheduler diario (excepciones vencidas) Una única tarea SIS con tabla wiki de todas las vencidas ese día y enlace de desactivación por cada una Nuevo ticket bulk + link_issues a cada SIS de activación + etiqueta Con aprobación

⚙ Configuración Jira

"jira": { "enabled": true, "base_url": "https://idealista.atlassian.net", "user_email": "[email protected]", "api_token": "<JIRA_API_TOKEN>", "project_key": "SIS" }

App Proxy y autenticación

Tres capas de autenticación: App Proxy Entra ID para SSO corporativo, MSAL auth code flow con claims challenge para operaciones Protected Actions (modo con aprobación), y token daemon vía SisScripts para ejecución directa (modo autoservicio).

🌐 App Proxy

  • Pre-auth: Microsoft Entra ID (no Passthrough)
  • SSO type: Header-based → inyecta X-MS-CLIENT-PRINCIPAL-NAME
  • URL externa: https://travelcaapprover-idealistaa82505660.msappproxy.net
  • URL interna: http://localhost:8091
  • Client ID: 6a0be082-1ff1-45ab-ad23-16dfc08db20f

👤 Roles de aplicación

Dos roles definidos en la App Registration y asignados en Enterprise App. Obtenidos vía GET /servicePrincipals/{sp_id}/appRoleAssignedTo y cacheados en sesión Flask.

RolAudienciaAcceso
Requester Helpdesk Crear solicitudes, ver estado propio
Approver Sysadmin Aprobar, deshabilitar, dashboard completo, trigger scheduler

⚙ Token daemon — SisScripts (250c7cb4)

En modo autoservicio, tanto las activaciones/desactivaciones manuales como el scheduler usan un token daemon (client credentials) de la App Registration compartida SisScripts. La sección daemon_graph en app-config.json apunta a este cliente; si no existe, se usa la sección auth como fallback.

CampoValor
Client ID250c7cb4-… (SisScripts App Registration)
Auth methodCertificado — thumbprint FC402921656C6BFE606980EB654C7A507883239E, leído de CurrentUser\My vía PowerShell subprocess
Permisos Application en SisScriptsPolicy.ReadWrite.ConditionalAccess, Policy.Read.All, Group.ReadWrite.All, User.Read.All

🔐 Permisos Graph

PermisoTipoApp RegistrationPara qué
Policy.ReadWrite.ConditionalAccess Delegated TravelCAApprover Crear y eliminar políticas CA y Named Locations (Protected Actions)
Policy.Read.All Delegated TravelCAApprover Leer políticas existentes para verificación
Group.ReadWrite.All Delegated TravelCAApprover Añadir y eliminar miembros del grupo CA – Travel Exceptions
User.Read.All Delegated TravelCAApprover Resolver UPN a displayName para construir nombres de los objetos CA
User.Read.All Application TravelCAApprover Obtener perfiles de usuario para autocompletar (daemon)
AppRoleAssignment.ReadWrite.All Application TravelCAApprover Leer assignments de roles de la app para autorización (daemon)
GroupMember.Read.All Application TravelCAApprover Listar miembros del grupo "Domain internal users" para autocompletar UPN
Policy.ReadWrite.ConditionalAccess Application SisScripts (250c7cb4) Crear y eliminar políticas CA y Named Locations en modo autoservicio
Policy.Read.All Application SisScripts (250c7cb4) Leer políticas para Named Location lookup — requerido junto a ReadWrite
Group.ReadWrite.All Application SisScripts (250c7cb4) Gestionar membresía grupo CA – Travel Exceptions en modo autoservicio
User.Read.All Application SisScripts (250c7cb4) Resolver UPN a displayName para nombre de los objetos CA (daemon)

Problemas encontrados y soluciones

Registro de los obstáculos técnicos encontrados durante el desarrollo y las soluciones aplicadas, como referencia para futuras intervenciones.

"No autorizado" en producción tras deploy

Síntoma

Todos los accesos devolvían 401. El header X-MS-CLIENT-PRINCIPAL-NAME no llegaba a Flask. En modo dev_bypass_upn funcionaba correctamente.

Causa raíz

App Proxy estaba configurado con pre-auth "Passthrough" en lugar de "Microsoft Entra ID". Sin pre-autenticación activa, Entra ID no inyecta los headers SSO.

Solución

Cambiar a pre-auth "Microsoft Entra ID" + SSO type "Header-based". La caché del navegador servía el 401 anterior — verificar siempre en ventana de incógnito tras cambios en App Proxy.

Roles vacíos — admin entraba como Requester

Síntoma

Un sysadmin con rol Approver correctamente asignado en la Enterprise App era tratado como Requester. El array de roles devuelto estaba vacío.

Causa raíz

GET /users/{upn}/appRoleAssignments pagina por defecto a 50 resultados. El usuario tenía más de 50 app role assignments en el tenant; el rol Approver de esta app quedaba en la página 2, que no se leía.

Solución

Cambiar a GET /servicePrincipals/{sp_id}/appRoleAssignedTo, que ya filtra por la app y devuelve solo los assignments de esta aplicación sin paginación problemática.

HTTP 400 en appRoleAssignedTo con $filter

Síntoma

Al intentar filtrar los assignments por principalId usando OData $filter, Graph API devolvía 400 Bad Request.

Causa raíz

El endpoint /servicePrincipals/{id}/appRoleAssignedTo no soporta OData $filter en la versión actual de Microsoft Graph API.

Solución

Traer todos los assignments sin filtro y filtrar en Python por principalId == object_id_del_usuario.

HTTP 403 en GET /users/{upn}

Síntoma

La app no podía resolver el UPN del viajero a displayName para construir el nombre de la Named Location. Error 403 Forbidden con token daemon.

Causa raíz

Faltaba el permiso application User.Read.All en la App Registration. El permiso delegado del mismo nombre no cubre las llamadas realizadas con el token daemon (client credentials).

Solución

Añadir User.Read.All (tipo Application) en "API permissions" de la App Registration y aplicar admin consent del tenant.

Protected Actions — 401/403 en operaciones CA

Síntoma

Las llamadas Graph para crear y eliminar políticas CA fallaban con 401 o 403 aunque el token era válido. La política CA #0002 del tenant requiere el claim acrs=c1 en el token.

Causa raíz

Las Managed Identities no pueden satisfacer Protected Actions porque no existe usuario interactivo capaz de hacer MFA step-up. Se investigaron y descartaron: desactivar la política #0002, política CA para workload identities, Workload ID Premium.

Solución

MSAL auth code flow delegado con client_capabilities=["CP1"] + parsing del claims challenge devuelto por Graph + redirección con parámetro claims=. El admin completa el MFA step-up y el nuevo token incluye acrs=c1.

Etiqueta Jira con espacios — HTTP 400

Síntoma

Al crear tickets Jira con la etiqueta "Travel CA Exception" (con espacios), la API devolvía 400 Bad Request. Los tickets se creaban sin etiqueta.

Causa raíz

La API de Jira Cloud no acepta etiquetas con espacios en blanco. Las etiquetas deben ser una sola cadena sin espacios.

Solución

Renombrar a Travel-CA-Exception (con guiones). Funciona correctamente en Jira y sigue siendo legible en los filtros y búsquedas.

GroupMember.Read.All para autocompletar

Síntoma

El autocompletar de UPN en el formulario de nueva solicitud fallaba con 403 al intentar listar miembros del grupo "Domain internal users" con el token daemon.

Causa raíz

GET /groups/{id}/members con credenciales de aplicación requiere explícitamente GroupMember.Read.All. El permiso delegado Group.ReadWrite.All no cubre esta lectura en modo daemon.

Solución

Añadir GroupMember.Read.All (tipo Application) en la App Registration + admin consent. Se aplica caché de 5 minutos dada la tamaño del grupo.

HTTP 403 en SisScripts — faltaba Policy.Read.All

Síntoma

En modo autoservicio, la llamada para crear la Named Location fallaba con 403 usando el token daemon de SisScripts, aunque el permiso Policy.ReadWrite.ConditionalAccess estaba correctamente concedido con admin consent.

Causa raíz

Microsoft Graph requiere Policy.Read.All junto a Policy.ReadWrite.ConditionalAccess para las operaciones sobre Named Locations con credenciales de aplicación (Application type). Sin el permiso de lectura, el endpoint devuelve 403 incluso aunque el de escritura esté concedido.

Solución

Añadir Policy.Read.All tipo Application en la App Registration SisScripts + admin consent del tenant. Verificar siempre que los permisos Application tienen admin consent aplicado (no basta con añadirlos).

WinSW no responde — restart del servicio por puerto

Síntoma

Restart-Service TravelCAApprover falló o dejó el servicio en estado inconsistente. El proceso Python seguía en memoria escuchando en el puerto 8091 bloqueando el reinicio.

Causa raíz

WinSW a veces no consigue terminar el proceso hijo antes de timeout, especialmente con APScheduler activo. El PID anterior persiste y el nuevo proceso no puede bindarse al puerto.

Solución

Matar el proceso directamente por puerto: $procId = (Get-NetTCPConnection -LocalPort 8091 -State Listen).OwningProcess; Stop-Process -Id $procId -Force. Nota: usar $procId, no $pid — en PowerShell $pid es variable reservada de solo lectura.

sqlite3.Row no tiene método .get()

Síntoma

AttributeError: 'sqlite3.Row' object has no attribute 'get' al ejecutar la desactivación automática en exec_auto_disable. La excepción se capturaba silenciosamente y la desactivación no se realizaba.

Causa raíz

sqlite3.Row solo soporta indexación con row["campo"], no el método dict row.get("campo"). El código usaba exc.get("jira_key") heredado de cuando el objeto era un dict.

Solución

Reemplazar exc.get("jira_key") por exc["jira_key"] if exc["jira_key"] else None. Regla general: en todos los módulos, acceder a campos de sqlite3.Row solo con corchetes, nunca con .get().

Stack técnico

Python 3 + Flask + Waitress, SQLite en WAL mode, WinSW para el servicio Windows. UI corporativa con navbar verde (#e1f56e) y botones magenta (#b62682), soporte dark/light theme.

📄 Ficheros del proyecto

FicheroDescripción
app.py Flask app principal: rutas, middleware SSO, filtro Jinja2 madrid_time para timestamps en Europe/Madrid
auth.py Token daemon MSAL (App Registration, client credentials) + auth code flow delegado + parsing del claims challenge para Protected Actions
graph.py enable() → 4 operaciones Graph con retry logic; disable() → 3 operaciones de borrado con reintentos; get_user_app_roles(); get_group_members()
db.py SQLite WAL mode, conexiones thread-local, auto-migración al arrancar, todas las operaciones de base de datos
jira_client.py Cliente REST Jira API v2 con todos los helpers de alto nivel: create_enable_ticket, add_approval_comment, add_disable_comment, add_cancel_comment, create_expiry_ticket
scheduler.py APScheduler con job diario de comprobación de expiración + run_now() para trigger manual desde el dashboard
templates/base.html Shell corporativo: navbar verde (#e1f56e), botones magenta (#b62682), soporte dark/light theme
templates/index.html Dashboard: lista de excepciones activas, pendientes e histórico, botón "Revisar vencidas", acciones por fila (aprobar, deshabilitar, cancelar)
templates/request.html Formulario nueva solicitud: autocompletar UPN, selector multi-país con 250 códigos ISO, campo referencia Jira
templates/approve.html Página de aprobación: muestra detalles de la excepción pendiente, botón "Ejecutar activación" que inicia el auth code flow
templates/processing.html Página de espera con spinner mientras se ejecutan las operaciones Graph; evita doble submit
templates/result.html Resultado de la activación o desactivación con resumen completo y enlace al ticket Jira
templates/error.html Página de error genérico con mensaje descriptivo y enlace de vuelta al dashboard
config/app-config.json Toda la configuración: host, puerto, Graph, Jira, auth MSAL, scheduler, SSO dev bypass

⚙ Claves de configuración (app-config.json)

ClaveDescripciónEjemplo / Valor
hostDirección de escucha0.0.0.0
portPuerto del servicio8091
db_pathRuta a la base de datos SQLitedata/travel_ca.db
auth.tenant_idTenant Entra IDd78b7929-c2a3-4897-ae9a-7d8f8dc1a1cf
auth.client_idApp Registration client ID6a0be082-1ff1-45ab-ad23-16dfc08db20f
auth.client_secretSecreto de la App Registration<SECRET>
auth.redirect_uriURI de callback MSAL (debe coincidir con la App Registration)https://travelcaapprover-...msappproxy.net/auth/callback
graph.domain_users_group_idID del grupo "Domain internal users" para autocompletar UPN<GUID>
graph.travel_exceptions_group_idID del grupo CA – Travel Exceptions9ef82868-2d3f-4abd-96eb-cfbc3783addc
graph.risky_countries_policy_idID de la política CA principal (solo referencia, no se modifica)9d0adb2b-ca25-4278-80ec-c627b3611bf8
graph.sp_idObject ID del Service Principal (Enterprise App) para leer roles<GUID>
jira.enabledHabilita o deshabilita toda la integración Jiratrue
jira.base_urlURL base del servidor Jirahttps://idealista.atlassian.net
jira.user_emailEmail del usuario de servicio Jira[email protected]
jira.api_tokenAPI token de Jira Cloud<JIRA_TOKEN>
jira.project_keyProyecto Jira destino para tareas generadas por la appSIS
daemon_graph.tenant_idTenant ID para el token daemon de autoservicio (SisScripts)d78b7929-c2a3-4897-ae9a-7d8f8dc1a1cf
daemon_graph.client_idClient ID de SisScripts App Registration250c7cb4-…
daemon_graph.thumbprintThumbprint del certificado en CurrentUser\MyFC402921656C6BFE…
sso.dev_bypass_upnUPN para saltarse el SSO en desarrollo (vaciar en producción)"" o "[email protected]"
scheduler.enabledActiva el scheduler APScheduler de expiracióntrue
scheduler.hourHora de ejecución del scheduler diario8
scheduler.timezoneZona horaria del schedulerEurope/Madrid
approval_required (BD SQLite settings)Toggle de modo: true = con aprobación + ticket SIS, false = autoservicio + token daemon. Cambiable en caliente sin reiniciar el serviciotrue / false

Estado actual del proyecto

Todas las funcionalidades planificadas están implementadas y verificadas en producción.

🔧 Implementación core

  • Script local PowerShell (Manage-TravelCAException.ps1) — Enable / Disable / List
  • Runbook Azure Automation (Invoke-TravelCAException.ps1) — para tenants sin Protected Actions
  • Protected Actions: investigación completa — MSAL CP1 + claims challenge
  • Web app travel-ca-approver — Enable y Disable verificados end-to-end
  • Deploy en producción vía WinSW + App Proxy
  • Roles Requester / Approver — asignación en Enterprise App
  • UI corporativa (navbar verde, botones magenta, dark/light theme)

🔗 Integraciones y funcionalidades

  • Autocompletar usuarios desde grupo "Domain internal users"
  • Selector multi-país con 250 códigos ISO
  • Cancelación de solicitudes pendientes
  • Integración Jira completa (enable, disable, cancel, expiry) — condicional según modo
  • Scheduler diario + trigger manual desde dashboard
  • Scheduler modo autoservicio: aplica desactivaciones directamente vía daemon
  • Toggle approval_required en SQLite — cambio en caliente sin reinicio
  • Token daemon SisScripts (250c7cb4) con autenticación por certificado
  • daemon_graph config section con fallback a auth
  • Timestamps en Europe/Madrid
  • SQLite WAL mode con auto-migración al arrancar