Skip to content

El alias colisionante con una service key se puede seguir creando, y las métricas lo resuelven case-insensitive #30

Description

@sre-helmcode

Seguimiento de nan#29, cerrado por cloud-api#120. Aquel PR impide operar con un alias colisionante, pero deja dos cabos.

1. El alias colisionante se sigue pudiendo crear

Ni el link de Discord (internal/handlers/account_discord.go:198) ni el trigger members_auto_handle (migración 018) consultan el allowlist. Así que el alias se crea igual y el miembro solo se entera al pulsar el botón de su key, recibiendo un 409 que no puede resolver por sí mismo.

El caso del handle es el peor de los dos: se autogenera del local-part del email, así que un alta con ccordova@… produce el alias reservado sin que el miembro haga nada, y UpdateMemberProfile (internal/db/queries/members.sql:282) excluye explícitamente handle, o sea que no hay forma de cambiarlo desde el portal.

Arreglo propuesto: consultar el allowlist en el punto donde el alias se elige (trigger de handle y link de Discord), en vez de solo donde se usa. El bucle de colisiones del trigger ya evita chocar con otros handles; le falta chocar con la lista de service keys.

2. Las métricas resuelven el alias por su cuenta, y de forma más ancha

MetricsHandler.resolveKeyAlias (internal/handlers/metrics.go:100) resuelve el alias sin pasar por el guard de resolveUser, y internal/metrics/litellmdb.go:124 compara case-insensitive:

LOWER(v.key_alias) = LOWER($1)

Eso es más ancho que la igualdad exacta del guard. Un miembro con alias colisionante vería en su dashboard el consumo de tokens y el gasto de la key de servicio. Arrastra por la misma resolución a metrics_sse.go y a internal/sync/usage.go.

Es solo lectura y agregado, y es preexistente, pero es el hueco que queda tras el #120.

3. Detección proactiva, si se quiere

Sin recuperar el WARN (que se bajó a Debug porque resolveUser corre en cada poll del portal), la alerta barata es:

HTTPRequestsTotal{method="GET", route="/api/keys", status="409"} > 0

Hoy ese 409 en GET /api/keys solo puede venir de este guard, así que aísla el caso con precisión y avisa del miembro encerrado sin ensuciar los logs. En POST comparte código con "API key already exists", así que ahí no discrimina.

Lo que se decidió NO arreglar

El guard corre antes de ensureEntitled, lo que lo convierte en un oráculo de enumeración del allowlist. Se deja así a propósito: explotarlo exige estar autenticado y cambiar el alias por intento, el premio es nulo (conocer el nombre de una service key no da acceso a nada, el allowlist la protege en todas las rutas mutantes), y el arreglo obvio es peor — mover el guard detrás de ensureEntitled haría que un miembro community viera el paywall en vez de la colisión, con la colisión esperándole intacta el día que pague.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions