Metadata-Version: 2.4
Name: tai-keycloak
Version: 0.3.1
Summary: Keycloak para servicios Python: validación local de tokens, login y administración de usuarios, grupos y roles; y tai-kc para levantarlo y desplegarlo
License-Expression: MIT
License-File: LICENSE
Author: MateoSaezMata
Author-email: msaez@triplealpha.in
Requires-Python: >=3.10,<4.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Provides-Extra: dev
Requires-Dist: click (>=8.1,<9.0)
Requires-Dist: httpx (>=0.27,<1.0)
Requires-Dist: jwcrypto (>=1.5,<2.0)
Requires-Dist: pydantic (>=2.7,<3.0)
Requires-Dist: pytest (>=8.0) ; extra == "dev"
Requires-Dist: pytest-asyncio (>=0.23) ; extra == "dev"
Requires-Dist: pytest-cov (>=5.0) ; extra == "dev"
Requires-Dist: pyyaml (>=6.0) ; extra == "dev"
Requires-Dist: rich (>=13.0)
Description-Content-Type: text/markdown

# tai-keycloak

Keycloak para servicios Python, y las herramientas para levantarlo y desplegarlo.

- **Validar tokens sin hablar con Keycloak**: firma contra el JWKS del realm (cacheado, con
  rotación de claves), emisor, tipo, audiencia y caducidad.
- **Login, refresco y logout** en nombre de un usuario.
- **Administrar usuarios, grupos, roles, clientes y el perfil de usuario** con un coste conocido
  por operación, sin N+1.
- **`tai-kc`**: Keycloak de desarrollo con Docker, un realm seguro por defecto y los ficheros
  para desplegarlo en cualquier orquestador, en Azure u on-premise con Traefik.

Es la pieza de la plataforma tai que usan las APIs que genera
[tai-api](https://github.com/triplealpha-innovation/tai-api) en modo Keycloak, y funciona igual
de bien sola.

```bash
pip install tai-keycloak
```

Python 3.10+. El runtime depende de `httpx`, `jwcrypto` y `pydantic`.

## En una API

```python
from tai_keycloak import InvalidTokenError, Keycloak, KeycloakSettings

kc = Keycloak(KeycloakSettings.from_env())   # no abre conexiones
realm = kc.realm("main")

async def current_user(authorization: str):
    try:
        claims = await realm.tokens.validate(authorization.removeprefix("Bearer "))
    except InvalidTokenError as error:
        raise HTTPException(401, error.message)
    return claims.preferred_username, claims.client_roles("api")

# al apagar
await kc.aclose()
```

`validate()` solo habla con Keycloak la primera vez (descarga el JWKS) y cuando llega un token
firmado con una clave nueva; el resto es CPU.

### Configuración

| Variable | Qué es |
|---|---|
| `MAIN_KEYCLOAK_URL` | URL del servidor (`https://auth.example.com`, `http://localhost:8090`, con ruta si la tiene) |
| `KEYCLOAK_API_CLIENT_SECRET` | Secreto del cliente `api`: login y cuenta de servicio para administrar |
| `KEYCLOAK_PUBLIC_URL` | URL con la que los clientes ven Keycloak, si no es la misma (emisor de los tokens) |
| `KEYCLOAK_ADMIN_USERNAME` / `KEYCLOAK_ADMIN_PASSWORD` | Administrador de `master`, solo para gestionar realms |
| `KEYCLOAK_API_CLIENT` / `KEYCLOAK_APP_CLIENT` | Nombres de los clientes (por defecto `api` y `app`) |
| `KEYCLOAK_VERIFY_SSL`, `KEYCLOAK_TIMEOUT` | Verificación del certificado y segundos por petición |

No hay credenciales por defecto: si falta algo, el error dice qué definir.

### Errores

Toda operación devuelve lo que promete o lanza un `TaiKeycloakError` con `message`,
`solution` y `status` (el código HTTP con el que respondería una API):

| Excepción | status | Cuándo |
|---|---|---|
| `InvalidTokenError` | 401 | Token caducado, manipulado, de otro emisor, sin la audiencia… |
| `InvalidCredentialsError` | 401 | Usuario o contraseña incorrectos (sin distinguir cuál) |
| `NotFoundError` / `ConflictError` | 404 / 409 | No existe / ya existe o el nombre es ambiguo |
| `InvalidRequestError` | 400 | Keycloak rechaza los datos (política de contraseñas, perfil…) |
| `PermissionDeniedError` | 403 | A la cuenta de servicio le falta un rol |
| `ServiceAuthenticationError`, `ConfigurationError` | 500 | Secreto o configuración incorrectos |
| `KeycloakUnavailableError` / `KeycloakServerError` | 503 / 502 | Keycloak no responde / falla |

## Administrar

```python
from pydantic import SecretStr
from tai_keycloak import GroupCreate, UserCreate, UserUpdate

await realm.groups.create(GroupCreate(name="operario", client_roles={"api": ["autor-read"]}))
await realm.users.create(UserCreate(username="ana", password=SecretStr("…"), groups=["operario"]))

page = await realm.users.list(group="operario", enabled=True, limit=50)   # items + total
ana = await realm.users.get("ana", effective=True)    # roles efectivos, heredados de sus grupos
await realm.users.update("ana", UserUpdate(password=SecretStr("…")))    # cierra sus sesiones
```

Lo que conviene saber:

- **Crear es atómico**: si un grupo o un rol no existe, el usuario no queda creado.
- **`UserUpdate` solo toca lo que se indica**: `attributes` se mezcla; grupos y roles son el
  conjunto final.
- **Cambiar la contraseña o deshabilitar a un usuario cierra todas sus sesiones.**
- **Keycloak 26 descarta en silencio los atributos que el perfil no declara**; la librería lo
  detecta y lanza el error. Los atributos de RLS se declaran con
  `realm.profile.set_rls_attributes([...])`.

En scripts y notebooks, la misma API bloqueante:

```python
from tai_keycloak.sync import SyncKeycloak

with SyncKeycloak() as kc:
    print(kc.realm("main").users.count())
```

Y para probar código que valida tokens sin un Keycloak, `tai_keycloak.testing.SigningKeys`
firma tokens como los de Keycloak y sirve su JWKS con un `httpx.MockTransport`.

## tai-kc

```bash
tai-kc init --mode development   # keycloak/: Dockerfile, compose, realm seguro, .env con secretos aleatorios
tai-kc run                       # Keycloak en http://localhost:8090
tai-kc check                     # ¿puede tu API hablar con él? dice qué falla y cómo arreglarlo
tai-kc realm new norte           # otro realm
tai-kc api users --group operario
tai-kc stop
```

Modos de `init`:

| Modo | Qué genera |
|---|---|
| `development` | compose con H2 en un volumen (o PostgreSQL con `tai-kc run --db postgres`) |
| `production` | la imagen `prod` optimizada y las variables que necesita, para cualquier orquestador |
| `azure` | lo anterior más un workflow de GitHub Actions que construye, sube a ACR y despliega en una Web App |
| `onpremise` | compose con Traefik (TLS con tus certificados) delante de Keycloak y PostgreSQL externo |

**Ningún secreto entra en una imagen**: contraseñas y secretos se pasan en runtime. El realm
generado no tiene redirecciones comodín, protege contra fuerza bruta, exige contraseñas de 12
caracteres, deja nombre y apellidos opcionales y da a la cuenta de servicio de la API solo los
roles que necesita. `init` nunca borra nada, y un `.env` existente no se pisa ni con `--force`.

## Desarrollo

```bash
pip install -e ".[dev]"
pytest                                                         # sin Keycloak: se saltan los de integración
TAI_KEYCLOAK_TEST_URL=http://admin:admin@localhost:8080 pytest # con un Keycloak real
```

Las reglas de diseño de la librería están en [`.claude/rules/`](.claude/rules/).

