Ir al contenido

Usuarios y acceso

La oficina no tiene registro abierto: entra quien tiene una fila activa en la tabla organtia.acceso_usuarios. Todo lo demás —el formulario, la cookie, el cierre de sesión— existe alrededor de esa tabla.

Cada instalación tiene su propia lista de usuarios. No hay una puerta común entre clientes y no hay invitados: si no estás en la tabla, o tu fila tiene activo = false, la puerta responde «Usuario o contraseña incorrectos» y no distingue tu caso de una contraseña mal escrita.

Situación Qué ocurre
Usuario en la tabla con activo = true y contraseña correcta Entra y se actualiza ultimo_acceso.
Usuario que no existe Se calcula igualmente un hash (para no delatar qué usuarios hay) y se cuenta el intento.
Usuario con activo = false Se trata como si no existiera.
Sin ACCESO_CLAVE en el entorno (o con menos de 32 caracteres) No entra nadie: la puerta falla cerrada, no abierta.

Conviene decirlo antes de buscar lo que no hay. No existen roles ni permisos por usuario. La tabla no tiene columna de rol, la aplicación no comprueba permisos distintos y todas las personas con cuenta ven y hacen lo mismo: la oficina entera, con sus fichas, sus aprobaciones y su configuración.

Lo que sí distingue a unas personas de otras es el rastro: la auditoría guarda el nombre del propietario o el usuario de la puerta en el campo actor de cada operación (ver /docs/administracion/auditoria/). Es trazabilidad, no control de acceso.

El CEO y los directores no son usuarios de esta tabla: son puestos, viven en organtia.puestos y no entran por la puerta.

Al entrar se crea una cookie llamada oficina_sesion con la forma usuario|expira|firma. La firma es un HMAC-SHA256 con la clave ACCESO_CLAVE; la caducidad la comprueba el servidor por su cuenta, porque el navegador puede mentir sobre la fecha.

La cookie se emite con HttpOnly, Secure y SameSite=Lax. Dura lo que diga ACCESO_DIAS:

Variable Por defecto Qué hace
ACCESO_CLAVE sin valor Clave con la que se firman las cookies. Debe tener 32 caracteres o más. Si cambia, todas las sesiones abiertas dejan de valer y hay que volver a entrar.
ACCESO_DIAS 7 Días de validez de la cookie. Se admite de 1 a 90; un valor fuera de rango o no numérico se queda en 7.

Caddy no sabe de contraseñas: antes de servir la oficina pregunta a /acceso/authz si hay sesión. Si la hay, recibe un 200 y la cabecera Remote-User con el usuario, que es lo que la aplicación muestra en la barra superior y lo que usa para atribuir las peticiones.

No se crea desde la web. Se crea con un script que va dentro del contenedor:

Ventana de terminal
docker compose exec -T api python scripts/crear_usuario_oficina.py pedro

Pide la contraseña dos veces por teclado, sin argumentos: lo que se escribe en la línea de comandos queda en el historial del shell y en ps. El usuario se guarda en minúsculas y el nombre visible se rellena con la primera letra en mayúscula.

Formas de uso:

Comando Qué hace
... crear_usuario_oficina.py pedro Crea el usuario o le cambia la contraseña si ya existe.
... crear_usuario_oficina.py --listar Lista usuario, si está activo, último acceso y nombre.
... crear_usuario_oficina.py --desactivar pedro Pone activo = false. No borra la fila ni sus datos.
... crear_usuario_oficina.py pedro --por-tuberia Lee la contraseña de la entrada estándar, dos líneas. Solo para automatizar.

Requisitos de la contraseña: 8 caracteres como mínimo. No hay reglas de mayúsculas, símbolos ni caducidad: el mínimo es bajo a propósito, para no estorbar al propietario.

El propio usuario la cambia en /acceso/clave, con la sesión abierta. Pide la actual, la nueva y la repetición, y comprueba que la nueva no sea igual que la actual. Al guardarla se renueva la cookie de ese navegador.

La contraseña no se guarda en ningún sitio: se guarda scrypt(clave, sal) con coste N=2^14, r=8, p=1 y una sal distinta por usuario. La comparación es en tiempo constante, y un usuario inexistente paga el mismo cálculo que uno real.

El botón de salir de la barra superior manda a /acceso/salir, que borra la cookie y vuelve al formulario.

Además, la interfaz vigila la inactividad: a los 30 minutos sin tocar nada (teclado, ratón, rueda o pantalla táctil) muestra un aviso con «Tu sesión se cerrará en un minuto si no haces nada» y un botón «Seguir aquí». Ese aviso no cierra la sesión por sí solo: la cookie sigue siendo la que manda. Lo que hace es, al volver a la pestaña, preguntar a la puerta si aún acepta la sesión; si ya no, aparece «La sesión se ha cerrado por seguridad. No se ha perdido nada» con un botón para entrar de nuevo.

Cinco intentos por usuario cada cinco minutos. Al sexto, la puerta responde igual que ante una contraseña incorrecta y escribe un aviso en el registro del proceso:

Acceso a la oficina bloqueado temporalmente: pedro

El contador vive en memoria: si se reinicia el servicio, se olvida. Es deliberado —lo que no valía era no tener nada— pero conviene saberlo antes de contar con él como defensa duradera.

Ventana de terminal
docker compose exec -T api python scripts/crear_usuario_oficina.py --desactivar pedro

Deja de poder entrar de inmediato. Lo que no hace es cortar la sesión que ya tenga abierta: la cookie se valida por firma y fecha, sin consultar la base de datos en cada petición, así que un navegador ya autenticado sigue dentro hasta que caduque (hasta ACCESO_DIAS días). Si necesitas cortar ya, cambia ACCESO_CLAVE y reinicia la API: eso invalida todas las cookies de todas las personas.

Por el mismo motivo, cambiar la contraseña a alguien no cierra sus otras sesiones.

organtia.acceso_usuarios (migración 028_usuarios_oficina.sql):

Campo Tipo Qué significa
id bigserial Identificador interno.
usuario text, único El nombre con el que se entra, en minúsculas. No es el correo.
nombre text Nombre visible. El script lo rellena con el usuario capitalizado; se conserva si se actualiza la contraseña sin dar nombre.
hash text Resultado de scrypt en hexadecimal. Nunca la contraseña.
sal text Sal de 16 bytes en hexadecimal, distinta por usuario.
activo boolean Si es false, la puerta lo ignora. Volver a guardar la contraseña lo pone en true.
creado_en timestamptz Alta.
ultimo_acceso timestamptz Última entrada correcta. Lo escribe cada autenticación; null significa que nunca ha entrado.

La tabla no tiene columna de doble factor todavía. El código la deja preparada para añadirlo encima sin tocar lo demás.

Para que no lo busques:

  • No hay alta ni baja desde la interfaz: solo el script.
  • No hay recuperación de contraseña por correo ni pregunta de seguridad. Si alguien la pierde, se la cambias con el script.
  • No hay doble factor propio.
  • No hay roles, grupos ni permisos por persona.
  • No hay caducidad de contraseña ni historial de contraseñas anteriores.
  • Los intentos de entrada no van a la tabla de auditoría: van al registro del proceso.
Lo que ves Causa probable Qué hacer
«Usuario o contraseña incorrectos» con la contraseña correcta Usuario desactivado, o espacio al final al pegarla Comprueba --listar y vuelve a escribirla a mano.
Entras y vuelves al formulario en bucle El navegador no guarda la cookie, o entras por http:// en vez de https:// (la cookie es Secure) Usa la dirección pública con HTTPS y acepta cookies del sitio.
A los cinco intentos fallidos no entra ni con la buena Bloqueo temporal de cinco minutos Espera cinco minutos o reinicia la API.
La puerta da error 500 y no muestra formulario Falta ACCESO_CLAVE o tiene menos de 32 caracteres Define la variable en .env y recrea el servicio api.
Cambiaste la contraseña y sigue entrando desde otro sitio La cookie de ese navegador no ha caducado Cambia ACCESO_CLAVE si necesitas cortar todas las sesiones.