Skip to main content

Cuentas de prueba (sd2 / aitha.es)

Cuentas dedicadas a QA manual del portal y sus subsistemas. No usar cuentas personales para probar: crear una cuenta paralela con el mismo email en otra org de Zitadel produce un usuario sin grants, y el portal responde 403 en todos los endpoints /api/admin/* sin explicar por qué (ver Identidades duplicadas).

claude-test — portal admin​

CampoValor
Loginclaude-test
Emailclaude-test@aitha.es (verificado)
PasswordCl4ude-T3st-Aitha-2026!
Zitadel user id388438852882137099
OrgAitha (367753979469299719)
ProyectoAitha (367754000994402311)
Rolessuadmin, admin, things, chat, customer_chat, things-mcp
User grant id388440117515124747
Portal (dashboard, host canonico)https://nex.aitha.es
Portada de accesohttps://portal.aitha.es
warning

Credencial de entorno de pruebas, no de producción de cliente. Rotar si se filtra fuera del equipo. No otorgar estos roles a cuentas de prueba en una instancia con datos reales de cliente.

Iniciar sesión​

Entrar por https://nex.aitha.es/login y usar claude-test como usuario — no el email. En esta instancia userLoginMustBeDomain está en false, así que los loginNames son el userName pelado, sin sufijo de dominio y sin relación con la dirección de correo.

Cuentas de aitoo, una por nivel de acceso​

aitoo no tiene roles propios: el nivel sale del user grant sobre el proyecto Aitha, que Zitadel emite en el claim urn:zitadel:iam:org:project:roles del id_token. mapRole() se queda con el primer rol que reconoce, asi que estas cuentas tienen exactamente uno cada una — con dos, el nivel efectivo depende del orden en que Zitadel serializa el claim y la prueba deja de ser reproducible.

LoginRol aitooZitadel user id
aitoo-liderlider389002524893380619
aitoo-accountaccount389002526805983243
aitoo-ejecutivoejecutivo389002528685031435
aitoo-finanzasfinanzas389002530882846731
aitoo-adminadmin389002532862558219

Password (las cinco): A1too-T3st-Aitha-2026!. Org Aitha, email <login>@aitha.es marcado verificado, buzones que no existen. Entrar por https://app.aitha.es con el loginName pelado.

Se crean y se reparan con:

./infra zitadel test-users sd2            # dry-run: dice que haria
./infra zitadel test-users sd2 --apply

Es idempotente: no pisa usuarios existentes y solo corrige el grant si el rol no es el esperado. La logica esta en deployment/scripts/python/zitadel_test_users.py.

warning

admin es un rol compartido de la plataforma, no de aitoo: cualquiera con admin en el proyecto Aitha — por ejemplo un admin del portal — entra a aitoo como admin. aitoo-admin existe para probar ese nivel de forma aislada, no porque sea un rol propio.

Modelo de identidad: orgs, tenants y roles​

Tres cosas distintas que se confunden fácil:

  • Org de Zitadel — frontera de identidad. Aitha (367753979469299719, la org por defecto de la instancia; se llamaba ZITADEL hasta el 2026-08-29), TOTEM (385958854862569495) y store-system (388129032832548877). Un mismo email puede existir como usuario separado en cada una.
  • Rol — user grant sobre el proyecto Aitha en Zitadel. Sin grant, el ID token no trae el claim urn:zitadel:iam:org:project:roles y el portal ve roles: [].
  • Tenant — fila en Postgres (user_tenants / client_tenants de user-service), indexada por el sub de Zitadel. No es la org.

El portal resuelve roles desde el ID token y tenants desde user-service. Los dos dependen del sub, así que un sub equivocado rompe ambos a la vez.

Un usuario, varios proyectos​

Un usuario de Zitadel pertenece a una sola org, pero puede tener user grants sobre proyectos de su propia org y sobre proyectos que otra org le delegó mediante un project grant. No hace falta duplicar la cuenta.

TOTEM vive en su propia org y su propio proyecto (385958923850481687, org TOTEM), separado del proyecto Aitha. Para que una cuenta de la org Aitha acceda a los dos hay un project grant 388441334198829067 (TOTEM → Aitha). Con eso pablo (367995470934441987) tiene los dos accesos desde un único login:

proyecto Aitha  -> admin, suadmin, things, chat, ...
proyecto TOTEM -> totem-viewer, totem-operator, totem-admin, totem-release-admin

Para sumar otra cuenta al proyecto TOTEM hay que citar el projectGrantId:

curl -s -X POST https://iam.aitha.es/management/v1/users/<USER_ID>/grants \
-H "Authorization: Bearer $PAT" -H 'Content-Type: application/json' \
-H 'x-zitadel-orgid: 367753979469299719' \
-d '{"projectId":"385958923850481687",
"projectGrantId":"388441334198829067",
"roleKeys":["totem-admin"]}'
Drift conocido

bootstrap.json declara una app TOTEM y roles totem-* dentro del proyecto Aitha, pero cellnex usa el client 388165560472502283 de la org TOTEM. Esa app y esos roles del proyecto Aitha no los usa nadie. Antes de tocar TOTEM, confirmar contra qué client apunta el deploy.

El portal esta fijado a la org Aitha​

Desde 2026-08-29 el portal manda urn:zitadel:iam:org:id:367753979469299719 en el authorize (ZITADEL_ORG_ID en platform/portal/.env.template), asi que solo autentica usuarios de la org Aitha. Una cuenta de TOTEM o de store-system es rechazada en el propio Zitadel:

400  El usuario no es miembro de la organización requerida (COMMAND-59ljd)

Si alguien reporta que no puede entrar al portal, ese error significa que su usuario vive en otra org — no que la contraseña este mal. La salida es crearle (o moverle) la cuenta en Aitha, no sacar el scope.

Ademas manda prompt=select_account, para que el authorize no reuse en silencio una sesion de Zitadel abierta por la tienda o por TOTEM. Sin eso el usuario entraba con una identidad que nunca eligio.

ThingsBoard tambien, pero se configura DENTRO de ThingsBoard​

El cliente OIDC que usa ThingsBoard no se declara en este repo: vive en la tabla oauth2_client de su propia base, creado a mano por la UI de TB. Por eso el scope de org hay que ponerlo ahi, y no aparece en ningun .env ni en bootstrap.json.

Sin ese scope Zitadel no filtraba nada: el selector de cuenta de ThingsBoard listaba sesiones de cualquier organizacion (una de cellnex incluida) y un usuario de otra org podia entrar. Corregido el 2026-08-31 agregando urn:zitadel:iam:org:id:367753979469299719 al scope, via la API de TB para que invalide sus caches:

U=$(docker exec core-portal-portal-ui-1 printenv THINGSBOARD_ADMIN_USERNAME)
P=$(docker exec core-portal-portal-ui-1 printenv THINGSBOARD_ADMIN_PASSWORD)
TOK=$(curl -s -X POST https://things.aitha.es/api/auth/login \
-H 'Content-Type: application/json' \
-d "{\"username\":\"$U\",\"password\":\"$P\"}" | jq -r .token)

# leer, agregar el scope al array, y reenviar el objeto COMPLETO por POST
curl -s https://things.aitha.es/api/oauth2/client/c002e0b0-3600-11f1-83b9-576da9192cdf \
-H "X-Authorization: Bearer $TOK"
curl -s -X POST https://things.aitha.es/api/oauth2/client \
-H "X-Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d @cliente.json
warning

El POST reemplaza el objeto entero, asi que hay que reenviar tambien el clientSecret que devuelve el GET (viene en claro, 64 chars) o se pierde.

Como el cliente no esta provisionado por codigo, si se reinstala ThingsBoard desde cero hay que volver a crearlo con este scope. Verificacion rapida:

curl -s -o /dev/null -D - \
'https://things.aitha.es/oauth2/authorization/c002e0b0-3600-11f1-83b9-576da9192cdf' \
| grep -i '^location'

El scope= de esa URL tiene que incluir urn:zitadel:iam:org:id:....

Pendiente

platform/chat-ui/src/lib/auth.ts comparte el client del portal pero no lleva ninguno de los dos parametros, y tampoco pide el scope urn:zitadel:iam:org:project:roles. Hoy no molesta porque chat.aitha.es no tiene router en Traefik, pero hay que arreglarlo antes de exponerlo.

Identidades duplicadas entre orgs​

Zitadel no deduplica por email entre orgs. Registrarse en la tienda o en TOTEM con un email que ya existe en la org Aitha crea un segundo usuario, con otro sub, sin grants y sin tenants. El scope de org de arriba evita que ese duplicado pueda entrar al portal, pero el duplicado se sigue creando: conviene revisar que store-system tiene allowRegister: true (Aitha y TOTEM no).

Caso real (2026-08-29): pablo@unlix.net existía como pablo (367995470934441987, org Aitha, con todos los roles) y como pablo@unlix.net (388169078822404107, org TOTEM, sin grants sobre Aitha). Entrar con el email resolvía al segundo → roles: [] → 403 en /api/admin/tenants. Se resolvió delegando el proyecto TOTEM a la org Aitha (project grant 388441334198829067) y borrando el duplicado, previa verificación de que ningún registro lo referenciaba en users_sync, chat_db, store_db, things_extensions, aitoo ni db_cellnex.

Detectarlo automáticamente:

ssh sd2 'sudo python3 -' \
< deployment/scripts/tests/zitadel_identity_audit.remote.py

Barre la instancia y sale != 0 si encuentra:

  1. un email presente en más de una org — marca cuál de los dos no tiene grants, que es el que daría 403;
  2. una persona de la org de identidad sin ningún grant ni membresía (los usuarios máquina se descuentan mirando las membresías de IAM/org, no adivinando por el nombre);
  3. orgs con allowRegister activo, que es lo que fabrica el caso 1.

Conviene correrlo después de tocar orgs, de dar de alta gente, o cuando alguien reporte un 403 raro. Está verificado contra un duplicado creado a propósito: lo detecta y sale con código 1.

A mano, si hace falta puntual:

PAT=$(sudo cat /opt/core2infra/security/zitadel/.runtime/management.pat)
curl -s -X POST https://iam.aitha.es/v2/users \
-H "Authorization: Bearer $PAT" -H 'Content-Type: application/json' \
-d '{"queries":[{"emailQuery":{"emailAddress":"alguien@ejemplo.com"}}]}'

curl -s -X POST https://iam.aitha.es/management/v1/users/grants/_search \
-H "Authorization: Bearer $PAT" -H 'Content-Type: application/json' \
-d '{"queries":[{"userIdQuery":{"userId":"<SUB>"}}]}'

Una respuesta sin result significa cero grants: ese usuario verá 403.

Smoke test de login​

deployment/scripts/tests/portal_login_smoke.remote.py hace el login real (NextAuth → Zitadel → callback) y barre los endpoints del dashboard. Un PASS significa que una persona puede entrar, no solo que los servicios responden.

ssh sd2 'sudo python3 -' \
< deployment/scripts/tests/portal_login_smoke.remote.py \
claude-test 'Cl4ude-T3st-Aitha-2026!'

Sale con código distinto de cero si el login falla o si algún endpoint devuelve >= 400. Corre en sd2 porque necesita el PAT del login client que vive dentro del contenedor zitadel-login-1.

Verificar los roles que llegaron a la sesión​

El portal loguea la extracción de roles en cada login:

ssh sd2 'docker logs --since 1h core-portal-portal-ui-1 2>&1 \
| grep -E "ROLE EXTRACTION|FINAL ROLES|TenantsAPI|\[Auth\] User"'

FINAL ROLES COLLECTED: 0 seguido de tenants: [] default=null es la firma del usuario sin grants.

Crear otra cuenta de prueba​

PAT=$(sudo cat /opt/core2infra/security/zitadel/.runtime/management.pat)
ORG=367753979469299719 # org Aitha
PROJECT=367754000994402311 # proyecto Aitha

# 1. Crear el usuario humano (email ya verificado para saltar el mail)
curl -s -X POST https://iam.aitha.es/v2/users/human \
-H "Authorization: Bearer $PAT" -H 'Content-Type: application/json' \
-d '{
"organization": {"orgId": "'"$ORG"'"},
"username": "mi-test",
"profile": {"givenName": "Mi", "familyName": "Test"},
"email": {"email": "mi-test@aitha.es", "isVerified": true},
"password": {"password": "<password-fuerte>", "changeRequired": false}
}'
# -> {"userId":"..."}

# 2. Darle roles (SIN esto el portal responde 403)
curl -s -X POST https://iam.aitha.es/management/v1/users/<USER_ID>/grants \
-H "Authorization: Bearer $PAT" -H 'Content-Type: application/json' \
-d '{"projectId": "'"$PROJECT"'", "roleKeys": ["admin","suadmin","things"]}'

Los roles disponibles están en security/zitadel/bootstrap.json (project.roles). Para que el portal muestre el panel de tenants alcanza con admin o suadmin.

Si además se necesita que la cuenta tenga tenants, hay que darle membresía en user-service (PUT /api/v1/users/{user_id}/tenants); los roles solos no crean tenants.