miércoles, 30 de abril de 2025

Gitlab(5): Problemas

1. Después de funcionar bien VS con gitlab durante una temporada, se produce este error:

Node.js v20.18.3

git@gitlab.municipio.es: Permission denied (publickey,password).

fatal: No s'ha pogut llegir del repositori remot.


Assegureu-vos que tingueu els permisos

d'accés correctes i que el repositori existeixi.


SOLUCIÓN: Parece ser que se ha perdido la clave ssh. Para ello, se busc el nombre del fichero de clave ssh (en mi caso es id_ed25519.ximo) y  se ejecuta:

ssh-add ~/.ssh/id_ed25519.ximo

NOTA: Cuando se crea un usuario dentro de Gitlab, se le puede crear una clave ssh para que no vuelva a pedir más la contraseña. Esta clave se guarda en la carpeta ~/.ssh

Esto sucede si en VS se ha configurado para acceder a gitlab con ssh. Para comprobar que estamos accediendo con ssh se ejecuta dentro de la carpeta principal del proyecto

git remote -v

y si el resultado comienza por origin  git@   entonces es ssh y si empieza por origin https entonces es https




martes, 29 de abril de 2025

INAP curso seguridad (II) Introducción al ENS

 1. Definiciones:

  • Política de seguridad:
  • Principios:
    • Asegurar acceso
    • Confidencialidad
    • Integridad
    • Trazabilidad
    • Autenticidad
    • Disponibilidad
    • Conservación de datos
  • Requisitos mínimos:

2. Principios básicos del ENS

  • Seguridad como proceso integeral
  • Gestión de la seguridad basada en riesgos
  • Prevención, detección, respuesta y conservacion
  • Gerstión de incidentes
  • Existencia de líneas de defensa en varias capas
  • Vigilancia continua
  • Reevaluación periódica
  • Diferenciación de responsabilidades: 
    • 3 niveles: 
      • Gobierno (dirección)
      • Supervisión (RESEG responsable de seguridad  y DPDP delegado de protección de datos).
      • Operativo (RS Responsable de sistemas, AD Administrador de seguridad y OS Operadores del sistema)
    • 3 roles: Dirección , responsable de información y responsable de servicio
    • Otros roles: Responsable de seguridad física, Responsable de gestión de personal, responsable de contratación y adquisición, usuarios del sistema. 
    • No se puede ser responsable de sistemas y de seguridead
    • La política de seguridad detallará las atribuciones de cada rol
    • 4 responsables:
      • Información
      • Servicio
      • Seguridad
      • Sistema (pueden ser varias personas delegadas)

lunes, 28 de abril de 2025

INAP curso seguridad (I) Protección de datos personales

 Créditos: https://sede.inap.gob.es/proteccion-de-datos-personales

1. Fuentes de derecho

Constitución española. Su artículo 18.4 otorga el carácter de derecho fundamental a la protección de datos de carácter personal.

Ley Orgánica 3/2018, de 5 de diciembre, de Protección de Datos Personales y garantía de los derechos digitales.

Reglamento (UE) n.º 2016/679 del Parlamento Europeo y del Consejo, de 27 de abril de 2016, relativo a la protección de las personas físicas en lo que respecta al tratamiento de datos personales y a la libre circulación de estos datos y por el que se deroga la Directiva 95/46/CE

2. Responsable del tratamiento de los datos de carácter personal (RTDP)

Es la persona física o jurídica, autoridad pública, servicio u otro organismo que, solo o junto con otros, determine los fines y medios del tratamiento (RUE 2016/679)

3. Delegado de protección de datos (DPD)

El RGPD establece además la obligación de designar un delegado de protección de datos (DPD) a toda autoridad u organismo del sector público que lleve a cabo tratamiento de datos personales

4. Registro de actividades de tratamiento

Primeramente de clasifican las actividades de tratamiento, que antiguamente correspodían a ficheros. Dichas actividades pueden ser:
  • Gestión tributaria y recaudatoria
  • Gestión de Personal
  • Servicios sociales
Para cada actividad se describen los siguientes datos:
  • Nombre de la actividad
  • Fecha de actualización
  • Responsable de tratamiento (normalmente es el responsable del tratamiento (visto en el punto 2)
  • Corresponsable: Normalmente la persona o departamento encargado de dicha actividad
  • Delegado de protección de datos (normalmente es la persona del punto 3)
  • Fines del tratamiento. La gestión de dicha actividad
  • Base legal: Normalmente la legislación que ampara dicha actividad (reglamento de reaudación, etc)
  • Catagoría de los interesados: Personas físicas, jurídicas ...
  • Categoría de los datos personales: (Datos identificativosm y de contacto, datos académicos, datos fiscales,..)
  • Categoría de los destinatarios:(Bases dwe convocatorias etc)
  • Transferencias internacionales: (solo cuando sw precean)
  • Medidas técnicas y organizativas de seguriead:Normalmente:Las medidas de seguridad corresponden a las aplicadas de acuerdo al Anexo II (Medidas de seguridad) del Real Decreto 3/2010, de 8 de enero, por el que se regula el Esquema Nacional de Seguridad en el ámbito de la Administración Electrónica.
Ejemplo para el INAP

5. Derechos del interesado. Vías para ejercer los derechos

Los derechos de los interesados se establecen en los artículos 15 al 22 del RGPD
Para ejercer los derechos se dirigirán a:
  • RTDP por vía electrónica o Red de oficinas en materia de registro (https://administracion.gob.es)
  • DPD (art. 38.4 RGPD)
  • AEPD (Agencia Española de protección de Datos) para reclamaciones




 

jueves, 24 de abril de 2025

Certificados: Instalar el certificado raiz en Chrome, Mozilla, Edge y Brave

1. Windows (Resto de navegadores -Chrome, Edge, Brave ..)



  1. Crear/Editar GPO

    • Abra Administración de Directivas de Grupo (gpmc.msc)

    • Vincule un GPO nuevo/existente a la OU objetivo

  2. Importar certificado al almacén del sistema
    Navegue a:
    Configuración del equipo → Políticas → Configuración de Windows → Configuración de seguridad → Directivas de clave pública → Entidades de certificación raíz de confianza

    • Click derecho → Importar

    • Seleccione el archivo .cer desde una ruta de red accesible (ej: \\servidor\certificados\rootCA.cer)

  3. Opciones clave de implementación

    • Almacén destino: Autoridades de Certificación Raíz de Confianza

    • Actualización automática: 90-120 minutos (default) o forzar con gpupdate /force

Verificación en Edge

  • Acceda a edge://settings/privacyAdministrar certificados

  • En Autoridades de certificación raíz, confirme la presencia del certificado

Notas técnicas:

  • Edge usa el almacén de certificados de Windows (Cert:\LocalMachine\Root)

  • Formatos compatibles: .cer, .crt, .der (evite .pem sin conversión)

  • Para instalaciones masivas:

certutil -f -addstore "Root" \\network\path\rootCA.cer


Este método garantiza compatibilidad con Edge 94+ y versiones Chromium-based, sin requerir configuraciones adicionales en el navegador[1][2][3].

  1. https://learn.microsoft.com/es-es/windows-server/identity/ad-fs/deployment/distribute-certificates-to-client-computers-by-using-group-policy

  2. https://techexpert.tips/es/windows-es/gpo-instalacion-de-un-certificado-raiz/

  3. https://woshub.com/how-to-deploy-certificate-by-using-group-policy/


2. Mozilla Firefox en Windows

Lo mas común es crear una GPO para que acepte el certificado raiz del repositorio de Windows. Como ya se instaló antes mediante una GPO, ahora hay que hacer otra GPO para que admita los certificados del repositorio de Windows. Veamos los pasos

1. Descargar las plantillas administrativas del almacén de Firefox
OJO: Según David hay que copiar dichas plantillas ern el SYSVOL compartido del servidor!!!!

2. Ejecutar en una consola de comandos gpmc.msc

3. Ir a una unidad organizativa (OU) de prueba y con el botón derecho indicar "Crear un GPO en este dominio y vincularlo aqui .. "

4. Darle un nombre por ejemplo EDU-Arrel-Firefox y editar

5. Ir a:
  • Configuración de equipo
  • Directivas
  • Plantillas administrativas: definiciones de equipo
  • Mozilla
  • Firefox
  • Certificates
  • Import Enterprise Roots : Habilitada


6. En los PCs de los usuarios hay que ejcutar gpupdate /force


OJO: Si entramos en Firefox a ver las entidades certificadoras raíz, NO APARECERÁ la nuestra, pues solo muestra los certificados de su base de datos y no los del repositorio de Windows aunque si hace referencia a ellos !!!

3. Corrección de Problemas

1. Verificar que en el cliente se han instalado las GPO

Ejecutar en el cliente en modo administrador gpresult /R /scope:computer 
 Y ver si se muestran las GPO aplicadas al equipo

2. Verificar que el certificado raíz esté instalado en al repositorio de windows del cliente.

En el cliente ejecutar en modo administrador:

certmgr.msc

Y vemos que se ha instalado en entidfades de certificación raiz de confianza:


Si entramos en Firefox a ver los certificados raiz instalados, NO APARECERÁ nuestro certificado raíz, pues Firefox se está nutriendo de 2 repositorios de certificados: el suyo propio y el de Wiindows. Cuando consulamos los certificados desde Firefox no aparecerá, pues aunque admite los certificados del repositorio de windows, NO LOS MUESTRA.

Si no va, no queda mas remedio que instalar manaulmente el certificado de la entidad certificadora.!!!1

NO USAR LO QUE VIENE A CONTINUACIÓN

1.1 Idea principal

Se tiene que crear el fichero "policies.json" en la carpeta de instalacioón del Mozilla Firefox: 

"C:\Program Files\Mozilla Firefox\distribution\"

que tendrá este contenido:

{
  "policies": {
    "Certificates": {
      "Install": ["C:\\ruta\\certificado\\rootCA.cer"]
    }
  }
}

Siendo "C:\\ruta\\certificado\\rootCA.cer" la ruta del retificdo raiz a instalar.


1.2 Utilizar una GPO

Lo más cómodo es utilizar una GPO. Vale la pena crear una OU de pruebas y meter un ordenador dentro.

Veamos el fichero power shell que se ejecuta como administrador de la máquina, y busca en las posibles rutas de instalación de Firefox y si lo encuentra crea este fichero para que el certificado raiz se instale en el Mozilla Firefox:

#Requires -RunAsAdministrator

# 1. Posibles rutas de instalación de firefox
$firefoxPaths = @(
    "C:\Program Files\Mozilla Firefox",
    "C:\Program Files (x86)\Mozilla Firefox"
)

# 2. Ruta del certificado a instalar
$certPath = "\\servidor\red\certificados\rootCA.pem"

# 3. Verificar ejecución como administrador
if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
    Write-Error "Ejecutar como Administrador"
    exit 1
}

try {
    # 4. Buscar instalación existente de Firefox
    $firefoxInstallPath = $firefoxPaths | Where-Object { Test-Path $_ } | Select-Object -First 1

    if ($firefoxInstallPath) {
        $distPath = Join-Path $firefoxInstallPath "distribution"
        
        if (-Not (Test-Path $distPath)) {
            New-Item -ItemType Directory -Path $distPath | Out-Null
        }
		# 5. Contenido del fichero json para copiar el certificado al directorio de distribución
        $jsonContent = @"
{
    "policies": {
        "Certificates": {
            "Install": ["$certPath"]
        }
    }
}
"@
        # 6. Escribir el fichero json en el directorio de distribución
		#    Para que al rearrancar firefox se instale el certificado
		$jsonContent | Out-File -FilePath "$distPath\policies.json" -Encoding utf8 -Force
		
		# 7. Copiar el certificado al directorio de distribución
		Copy-Item -Path $certPath -Destination $distPath -Force

		# 8. Crear el fichero policies.json en el directorio de distribución
		#    y escribir el contenido del json en él
		$jsonContent | Set-Content -Path "$distPath\policies.json" -Force
        Write-Output "Política aplicada en: $distPath\policies.json"
    }
    else {
        Write-Warning "Firefox no está instalado en rutas estándar"
    }
}
catch {
    Write-Error "Error en la implementación: $_"
    exit 1
}

ahora guardamos el fichero como Deploy-FirefoxCert.ps1

Ahora vamos al Windows 2019 server y en el Administrador de Directivas de Grupo:

  • Crear nueva GPO > Editar
  • Navegar a: Configuración del equipo/Preferencias/Configuración de Windows/Tareas programadas 
  • Crear tarea:
    • Acción: Iniciar programa
    • Programa/script: powershell.exe
    • Argumentos: -ExecutionPolicy Bypass -File "\\ruta\netlogon\Deploy-FirefoxCert.ps1"
    • Ejecutar con privilegios más altos: Habilitado
Vincular GPO a la OU correspondiente


Verificación post-implementación:

En cliente Windows, abrir "about:policies en Firefox" y verificar certificado en "about:certificate"

Consideraciones clave:

El certificado debe estar accesible desde todas las estaciones ([ruta de red])

Para actualizaciones de certificado, modificar el archivo rootCA.pem y forzar reimplementación GPO


martes, 22 de abril de 2025

Python(XXVII). Aplicando el ENI (II). Comprobación de las entradas de datos. FastHTML beforeware

 1. Beforeware en FastHTML

Cuando creamos una aplicacion FastHTML utilizando app = FstHTML(), podemos pasar un parámetro que es "before=Beforeware(función)" siendo "función" el nombre de una función que hemos definido para ejecutarse justo antes de ejecutar una ruta. Si la función devuelve "None", la petición sigue normalmente. Andreas Stokl lo explica muy bien.

Uno de nuestros objetivos a verificar es que el "tab_id" sea el correcto y que tengamos un usuario correcto..Para ello, sino se cumplen estos requisitos, se redirigirá a la pantalla de login.

Otra cosa importante, es decirle al beforeware que regex hay que saltarse "skip"para que no haga comprobaciones en cada caso. En AnswerDotAi se puede ver mirando el comentario del código.

# To create a Beforeware object, we pass the function itself, and optionally a list of regexes to skip.
bware = Beforeware(before, skip=[r'/favicon\.ico', r'/static/.*', r'.*\.css', '/login'])




martes, 15 de abril de 2025

Python (XXVI) Sqlalchemy: Localizar e importar todas las clases a persistir. Modificar las tablas cuando la clase asociada cambia

1. Importar todas las clases a persistir con sqlalchemy

Para ello debemos tener todos los módulos que definen las clases dentro de una carpeta, en mi caso "models"

Creamos un módulo dentro de la carpeta "models" que recoja toda las clases y así solo tenemos que importar el array de clases a persistri.

El módulo se llama xmallmodels.py, y la variable que contendrá la clases es model_classes  y su código es:

import importlib
import pkgutil
import inspect
#------Imprescindible para poder importar de otras carpetas (de basicutils)
import sys
from pathlib import Path
path_root1 = Path(__file__).parents[1] # Damos un salto de directorio hacia arriba-> ximoutilsmod
sys.path.append(str(path_root1))
path_root0 = Path(__file__).parents[0] # El mismo directorio
sys.path.append(str(path_root0))
from basicutils.xmdb import Base
import models
# ------Fin imprescindible

model_classes = []

for loader, module_name, is_pkg in pkgutil.iter_modules(models.__path__):
    module = importlib.import_module(f"models.{module_name}")
    
    for name, obj in inspect.getmembers(module, inspect.isclass):
        # Ensure it's defined in the current module and is a subclass of Base (but not Base itself)
        if obj.__module__ == module.__name__ and issubclass(obj, Base) and obj is not Base:
            model_classes.append(obj)

# Example: register or print them
for cls in model_classes:
    print(f"Found model class: {cls.__name__} from {cls.__module__}")


Por ejemplo en nuestro programa de arranque, que intenta vincular los modelos con la BD, basta con importar model_classes desde este módulo. El código de este módulo mnu_main.py es:

#!/home/eduard/MyPython/11.softprop-01/venv_softprop/bin/python3

#1. Imports
from sqlalchemy import Table
#------Imprescindible para poder importar de otras carpetas (de basicutils)
import sys
from pathlib import Path
path_root1 = Path(__file__).parents[1] # Damos un salto de directorio hacia arriba-> ximoutilsmod
sys.path.append(str(path_root1))
path_root0 = Path(__file__).parents[0] # El mismo directorio
sys.path.append(str(path_root0))
from menus.mnu_fh import fh, app
#OJO: No eliminar las dependencias marcadas en gris (routes01mnu, routes02form, routes03grid,app) pues, sinó falla el programa
from menus import routes00comp, routes01mnu, routes02form, routes03grid # No eliminar !!!
from menus.mnu_fh import fh, app                         # No eliminar app !!!
from basicutils import xmdb
#--- Definición de tablas de la Base de datos postgres
from models.xmallmodels import model_classes
# ------Fin imprescindible

from basicutils import xmdb 

# Create database tables
xmdb.Base.metadata.create_all(bind=xmdb.engine)

# Execute the web server, but now not using fh.serve but uvicorm.run instead
#fh.serve()
if __name__ == "__main__":
	import uvicorn
	app.mount("/static", fh.StaticFiles(directory="/home/eduard/MyPython/11.softprop-01/static"), name="static")
	#uvicorn.run(app, host="0.0.0.0", port=5001, 
	#    ssl_keyfile="/home/eduard/MyPython/11.softprop-01/static/certs/wildcard2023Nginx.rsa", 
	#    ssl_certfile="/home/eduard/MyPython/11.softprop-01/static/certs/wildcard2023Nginx.crt")

	cert_path="/home/eduard/MyPython/11.softprop-01/static/certs/wildcard.municipio.es."
	#uvicorn.run(app, host="edu.tavernes.es", port=5001,	
	uvicorn.run(app, host="192.168.XXX.XXX", port=5001,	
		ssl_keyfile =cert_path+"key", 
		ssl_certfile=cert_path+"crt")


2. Modificar las tablas cuya clase asociada cambia

Para ello se buscan las clases que deriven de la clase Base, y si estan asociadas a una tabla (tienen el campo __tablename__) y se compara los atributos de la clase con las columnas de la tabla; y se añaden columnas  o se eliminan en base a los atributos de las clases.

Veamos el código del módulo xmdbupdate.py

''' Actualización de la estructura de tablas en función
     de las clases asociadas de SQLAlchemy.
'''

from sqlalchemy.sql import text
from sqlalchemy.inspection import inspect
from sqlalchemy.exc import ProgrammingError


#------Imprescindible para poder importar de otras carpetas (de basicutils)
import sys
from pathlib import Path
path_root1 = Path(__file__).parents[1] # Damos un salto de directorio hacia arriba-> ximoutilsmod
sys.path.append(str(path_root1))
path_root0 = Path(__file__).parents[0] # El mismo directorio
sys.path.append(str(path_root0))
#OJO: No eliminar las dependencias marcadas en gris
#--- Definición de tablas de la Base de datos postgresfrom basicutils.xmdb import Base 
from basicutils import xmdb
from models.xmallmodels import model_classes
# ------Fin imprescindible

#-----------------------------------------------------------
# --- Configuration ---
session = xmdb.session_local()

def all_table_subclasses(cls):
    """Recursively find all subclasses of a class that has a table assigned."""
    subclasses = set()

    for subclass in cls.__subclasses__():
        print (subclass.__name__,getattr(subclass, '__tablename__', ''), getattr(subclass, '__abstract__', False))
        if len(getattr(subclass, '__tablename__', '')) > 0:
            subclasses.add(subclass)
        subclasses.update(all_table_subclasses(subclass))

    return subclasses



# --- Update function ---
def update_table_structure(cls):
    table_name = cls.__tablename__
    table_args = cls.__table_args__
    schema = table_args.get('schema') if isinstance(table_args, dict) else None
    inspector = inspect(xmdb.engine)

    if not inspector.has_table(table_name, schema=schema):
        print(f"Table '{schema}.{table_name}' does not exist. Creating it...")
        cls.__table__.create(xmdb.engine)
        return

    db_columns = {col["name"]: col for col in inspector.get_columns(table_name, schema=schema)}
    model_columns = {col.name: col for col in cls.__table__.columns}

    #Execute a transaction
    with xmdb.engine.begin() as conn:
        # Add missing columns
        for name, column in model_columns.items():
            if name not in db_columns:
                col_type = column.type.compile(xmdb.engine.dialect)
                nullable = "NULL" if column.nullable else "NOT NULL"
                alter = f'ALTER TABLE "{schema}"."{table_name}" ADD COLUMN "{name}" {col_type} {nullable};'
                print("Adding:", alter)
                conn.execute(text(alter))

        # Drop extra columns
        for name in db_columns:
            if name not in model_columns:
                alter = f'ALTER TABLE "{schema}"."{table_name}" DROP COLUMN "{name}";'
                print("Dropping:", alter)
                try:
                    conn.execute(text(alter))
                except ProgrammingError as e:
                    print(f"Could not drop column {name}: {e}")

# --- Run for all subclasses of Base for updating the tables---
def update_all_tables(base):
    for cls in all_table_subclasses(base):
        update_table_structure(cls)

if __name__ == "__main__":
	update_all_tables(xmdb.Base)
	session.close()
	xmdb.engine.dispose()
	




jueves, 10 de abril de 2025

Python(XXV). Aplicando el ENI (I). Doble factor de autenticación 2FA

0. Introducción

El 2FA cont TOTP consiste en :
  1. Generar una clave que solo se comparte una vez, un usuario y un servicio
  2. El proceso de compartición de la clave se hace una sola vez, ya sea la primera vez o cuando se cambia de teléfono (que es el que guarda la clave)
  3. El usuario tiene que ingresar un código, generalmente de 6 dígitos que cambia cada 30 segundos
  4. Este código a ingresar se calcula en base a la clave, el usuario, el servicio y la hora UTC de sincronización.
Si el usuario no existe en la BD, es cuando se muestra el código QR donde se desvela la clave secreta y los demás campos. Dicho QR contiene esta información

otpauth://totp/NOMBRE_SERVICIO:usuario?secret=CLAVE&issuer=NOMBRE_SERVICIO

La seguridad del mismo consiste en que la clave solo la saben el usuario y el servidor, y el código a ingresar cambia cada 30 segundos.

Lo único que sirve para generar el código es la cloave secreta. El usuario y servicio solo sirven para tener una referencia. Es importante que los sistemas estén sincronizados con un servidor horario para obtener la hora exacta. 

1. Instalar librerías PYOTP y QRCODE[PIL]

La librería pyotp sirve par generar y verificar contraseñas de un solo uso

La librería qrcode[pil] es para generar códigos QR.

Se instalarán así

pip install pyotp qrcode[pil]

2. Código python simple para ver como funciona TOTP

El siguiente código nos explica el proceso, desde la obtención de las claves, usuario y servicio a la generación del código QR (que solo se debe mostrar una vez) hasta la entrada del código de 6 dígitos

# 1.Definir el nombre del servicio
service=MyService@XimoDante

# 2. Obtener el nombre del usuario
user=myuser

# 1. Generar una clave secreta aleatoria en Base32
secret = pyotp.random_base32()

# 2. Inicializamos el objeto TOTP (Time-Based One-Time Password) 
#    que es el encargado de crear los dígitos de verificación y verificarlos
totp = pyotp.TOTP(secret)

uri=totp.provisioning_uri(name=user,
    issuer_name=service
)

# 3. Imprimimos la URI para ver el resultado
print ("URI="+uri)

# 4. Generamos el QR y la guardamos
qrcode.make(uri).save('totp.png')

# 5. Entrar en el Google Authenticator y escanear el QR por primera vez
#    El Authenticator nos dará un código de 6 dígitos que tenemos que introducir como doble factor

# 6. Ingresar el código 
while True:
    print(totp.verify(input('Ingresar el código: ')))


3. Definición del modelo de usuarios 

Los usarios se deben guardar en un BD, con ello comprobamos si ya se les ha dado el código QR. Si no se les ha dado el QR, hay que generarlo para dicho usuario y guardar en la BD la clave secreta del Authenticator que deberá ser distinta para cada usuario. Observar la parte dedicada solo a TOTP. En nuestro cado concreto el módulo de clase es es:

from enum import Enum
from datetime import datetime, timezone
import pyotp
from sqlalchemy import Boolean, Column, DateTime, Integer, String, Text

# ------Imprescindible para poder importar de otras carpetas (de basicutils)
import sys
from pathlib import Path
path_root1 = Path(__file__).parents[1] # Damos un salto de directorio hacia arriba-> ximoutilsmod
sys.path.append(str(path_root1))
from basicutils.xmdb import Base, myschema
# ------Fin imprescindible

TABLE_ARGS={'schema': myschema}

class UserAbst(Base):
	__abstract__= True # Prevents this class from being used as a table
	# 1. Datos Sedipualba
	id = Column(Integer, primary_key=True, autoincrement=True, index=True)
	description = Column(String, unique=True, index=True, nullable=False) # tab_id
	id_alba = Column(Integer, nullable=False, default=0, index=True) 
	nif = Column(String, default='', index=True)
	nom = Column(String, default='', index=False)
	cognom1 = Column(String, default='')
	cognom2 = Column(String, default='') 
	puesto = Column(String, default='', index=True)
	activo = Column(Boolean, default=False)
	
	# 2. Datos LDAP
	ldap_user = Column(String, default='', index=True)
	ldap_cn = Column(String, default='')
	ldap_mail = Column(String, default='')
	ldap_activo = Column(Boolean, default=False)
	ldap_user_control = Column(String, default='')
     
	# 3. Datos TOTP (Authenticator)
	totp_secret = Column(String, default=lambda: pyotp.random_base32())
	totp_user = Column(String, default='', index=True)
	totp_service=Column(String, default='Softprop@Tav')
	totp_activated = Column(Boolean, default=False)
    
	# 4. Fecha de creación/modificación
	session_date = Column(DateTime, default=lambda: datetime.now(timezone.utc))

class UserStore(UserAbst):
    __tablename__ = "x_users"
    __table_args__ = TABLE_ARGS
    

class UserStoreHst(UserAbst):
    __tablename__ = "x_users_hist"
    __table_args__ = TABLE_ARGS


4. Pantalla de login y verificación 

Usaremos una pantalla que pida usuario y contraseña y dejaremos un "div" cuyo id es "totp_div", que por htmx añadiremos el código QR para authenticator (solo la primera vez) y un campo que pida el código a introducir. Veamos el xmloginform.py

import base64
import os
from fasthtml import common as fh


#------Imprescindible para poder importar de otras carpetas (de basicutils)
import sys
from pathlib import Path

path_root1 = Path(__file__).parents[1] # Damos un salto de directorio hacia arriba-> ximoutilsmod
sys.path.append(str(path_root1))
path_root0 = Path(__file__).parents[0] # El mismo directorio
sys.path.append(str(path_root0))
from basicutils import xmother, xmfunction
# ------Fin imprescindible



def login_form():
	''' Simple login form '''
	# 1. js to save the tab:id in th session storage
	with open(f"{xmfunction.get_project_path()}/static/js/form_script.js", newline='') as f: myjs=f.read()
	
	# 2. Crete the random string of the tab_id 	
	tab_id = xmother.get_random_string()

	# 3. Fields in the form
	fields=['usuari','paraula_de_pass','tab_id']
	buttons=[
		{'name':'Identificar-se', 'type':'submit', 'cls':'btn btn-primary'},
		{'name':'Cancel·lar'    , 'type':'reset' , 'cls':'btn btn-secondary'}
	]	
	
	return fh.Div(
		fh.Script(code=myjs), # Este js guarda en la session store el tab_id com id_tab
		fh.H2("Identificar-se al sistema"),
		fh.Form(
			*[
				fh.Div(
					fh.Label(field.capitalize().replace("_"," ") , **{'cls':"form-label", 'for':field}) if field !='tab_id' else '',
					fh.Input(
						type="hidden" if field=='tab_id' else "text", 
						value=tab_id if field=='tab_id' else "",
						** {'name':field, 'id':field, 'cls':"form-control"}),
					cls="mb-3" 
				) for field in fields 
			],
			fh.Div(
				fh.Div(id="error-message", cls='text-danger text-center mb-3'),
				id="totp_div"
			),	
			fh.Div(
				*[fh.Button(button.get("name"), ** button) for button in buttons ],
				cls="d-flex justify-content-between"
			),
			id="my_form",
			# COMMENT:hx_post="/softprop/validate_login?tab_id="+tab_id, hx_target="#error-message", hx_swap="innerHTML"
			hx_post="/softprop/validate_login?tab_id="+tab_id, hx_target="#totp_div", hx_swap="innerHTML"

			
		),
		#fh.Form(id="logout-form", style="display-none", action="/logout", method="POST"),
		cls="card shadow-lg p-4", style="width: 350px;",
		
	)

Se comprueba el usuario y contraseña y si es correcto se pide el id, para ello la url "/softprop/validate_login?tab_id=..." se encarga de añadir los campos que hacen falta, pero siempre comprobando el "tab_id". Para ello el código del módulo routes01mnu.py queda:


#1. Imports
import base64
from datetime import datetime
import json
import os
from fasthtml import common as fh
from dataclasses import dataclass

#------Imprescindible para poder importar de otras carpetas (de basicutils)
import sys
from pathlib import Path
#from sqlalchemy.orm import Session
#import urllib
path_root1 = Path(__file__).parents[1] # Damos un salto de directorio hacia arriba-> ximoutilsmod
sys.path.append(str(path_root1))
path_root0 = Path(__file__).parents[0] # El mismo directorio
sys.path.append(str(path_root0))
from basicutils import xmfunction, xmdb, xmldap, xmother, xmloginform, xmtotp
from session import xmsession
from menus import mnu_ui
from menus.mnu_fh import app
from models.xmsessionmodels import SessionExistence
# ------Fin imprescindible

@dataclass
class Login: usuari: str; paraula_de_pass: str; tab_id:str; totp_digits: str | None = None


@app.get("/softprop/login")
def get_login():
	''' Primero muestra el formulario de login sin vampos de TOTP'''
	a=xmloginform.login_form() 
	#print(fh.to_xml(a))
	print('passant per /softprop/login')
	return a 

@app.post("/softprop/validate_login")
def post_login(req, login: Login):
	# -------- COMPROBACION DE ERRORES -------
	# 1. Si no autenticamos al usuario con el login y la password
	#    Le devolvemos un error
	ldap_authenticated= xmldap.autheticate_by_login(login.usuari, login.paraula_de_pass)
	if not ldap_authenticated:
		print ("returning to first login page")
		return fh.Div("Error en l'usuari o la paraula de pas", cls="alert alert-danger")

	# 2. Comprobamos si tiene TOTP
	has_totp, totp, image_path=xmtotp.get_totp(login.usuari)
	
	# 3. Si no ha introducido el código TOTP mostramos la pantalla 
	#    para que lo ingrese
	no_digits=login.totp_digits is None
	if no_digits:
		print("Mostrando el input para introducir el TOTP")
		return xmother.get_totp_html(fh, has_totp, image_path)

	# 4. No ha introducido el TOTP correcto
	totp_correcto= xmtotp.validate_totp(totp, login.totp_digits, login.usuari)
	if not totp_correcto:
		print("Mostrando otra vez el input para introducir el TOTP")
		return xmother.get_totp_html(fh, has_totp, image_path, True)	
	
	#------ VAMOS A LA PANTALLA DE MENU ----------------
	#5. Si ha introducido el código TOTP correcto
	#   Mostramos el menu
	a_user=''
	with xmdb.session_local() as db:
		a_user, myerror=xmsession.set_session(req=req, db=db, user=login.usuari, tab_id=login.tab_id)
	
	if len(a_user.strip())==0: 
		print ("returning to the first login page")
		return fh.Div("Error al crear la sessió: "+myerror, cls="alert alert-danger")
	
	ck_dict={'httponly':True, 'max_age':3600}
	tab_id_dict={ "username": login.usuari }
	# COMMENT: sec_dict={'secure': True}
	cookies_dicts=[
		{'key':login.tab_id, 'value':json.dumps(tab_id_dict)} | ck_dict,
	]	
	redirection='/softprop/tree?tab_id='+login.tab_id
	a=fh.RedirectResponse(redirection, status_code=303)
	for cookie in cookies_dicts:
		a.set_cookie(**cookie)
	print('passant per /softprop/validate_login i anem a /softprop/tree ja que ens hem validat')	
	return a
		

@app.get("/softprop/tree")
def geto2(tab_id:str | None= None, req=None):#request:Request): 
	''' Execute the main view that is tree menu'''
	
	# 1. Validem que haja tab_id i que l'usuari estiga en la sessió de la BD
	my_response,myerror=xmsession.validate(tab_id=tab_id, req=req)
	if (len(myerror.strip())>1):
		print ("redirecting from /softprop/login" )
		return my_response
	

	menu_dict=mnu_ui.get_menu_dict()  
	my_response=mnu_ui.get_general_tree_view_code_fh(menu_dict=menu_dict)
	print("========================================================")
	print('passant per /softprop/tree')	
	#print(fh.to_xml(my_response))
	print("========================================================")
	return my_response

	


Como se ve, si todo es correcto se redirecciona a "/softprop/tree". Si no se ha hecho ninguna entrada previa en authenticator, muestra el código QR también.