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
| Campo | Valor |
|---|---|
| Login | claude-test |
claude-test@aitha.es (verificado) | |
| Password | Cl4ude-T3st-Aitha-2026! |
| Zitadel user id | 388438852882137099 |
| Org | Aitha (367753979469299719) |
| Proyecto | Aitha (367754000994402311) |
| Roles | suadmin, admin, things, chat, customer_chat, things-mcp |
| User grant id | 388440117515124747 |
| Portal (dashboard, host canonico) | https://nex.aitha.es |
| Portada de acceso | https://portal.aitha.es |
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.
| Login | Rol aitoo | Zitadel user id |
|---|---|---|
aitoo-lider | lider | 389002524893380619 |
aitoo-account | account | 389002526805983243 |
aitoo-ejecutivo | ejecutivo | 389002528685031435 |
aitoo-finanzas | finanzas | 389002530882846731 |
aitoo-admin | admin | 389002532862558219 |
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.
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 llamabaZITADELhasta el 2026-08-29),TOTEM(385958854862569495) ystore-system(388129032832548877). Un mismo email puede existir como usuario separado en cada una. - Rol — user grant sobre el proyecto
Aithaen Zitadel. Sin grant, el ID token no trae el claimurn:zitadel:iam:org:project:rolesy el portal veroles: []. - Tenant — fila en Postgres (
user_tenants/client_tenantsde user-service), indexada por elsubde 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"]}'
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
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:....
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:
- 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; - 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);
- orgs con
allowRegisteractivo, 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.