# Permission Enforcement — Guía de Integración

## Archivos Modificados

| Archivo | Qué cambió |
|---------|-----------|
| `common/auth-helpers.php` | +9 whitelist queries, +clase `PermissionCache`, +5 funciones públicas |
| `services/education/v1/homework/index.php` | `TODO` → `requirePermission()` en GET y POST |
| `frontend/shell/gx-page.php` | Sección 3 ahora carga permisos reales via `PermissionCache` |

## Cómo funciona

```
Cookie auth:   JWT → id_core_profile → PROFILE.id_role → PERMISSION_ROLE ± PERMISSION_PROFILE → ∩ PLAN_PERMISSION
API Key auth:  Bearer key:secret → API_KEY_PERMISSION → ∩ PLAN_PERMISSION
```

Todo se resuelve en una sola ronda de queries (batch) y se cachea en memoria estática por request. Después de la primera llamada, cada `requirePermission()` es O(1).

## Uso en endpoints

```php
// En cualquier endpoint, después de requireAuth():
requireAuth();
$companyId = idCompany(true);

// Exigir UN permiso (403 si no lo tiene)
requirePermission('homework.view');

// Exigir TODOS (403 si falta alguno)
requirePermission('homework.view', 'homework.export');

// Exigir AL MENOS UNO
requireAnyPermission('homework.edit', 'homework.create');

// Check sin 403 (para lógica condicional)
if (hasPermission('homework.export')) {
    // incluir datos de exportación
}

// Obtener id_role resuelto
$roleId = idRole(); // null para API Key auth

// Obtener todos los permisos (para respuestas que lo necesiten)
$perms = allPermissions(); // ['homework.view' => true, ...]
```

## Uso en frontend (gx-page.php)

Ya está conectado. Después de incluir `gx-page.php`, tienes:

```php
<!-- PHP -->
<?php if (gxCan('homework.create')): ?>
  <button>Nueva tarea</button>
<?php endif; ?>
```

```javascript
// JS — GX.page.permissions ya incluye los permisos reales
if (GX.page.can('homework.create')) {
  showCreateButton();
}
```

## Configuración

### Variable de entorno: `PERMISSION_REQUIRE_PLAN`

En `.env_internal` o `.env_module`:

```bash
# Producción (default): sin plan activo = sin permisos
PERMISSION_REQUIRE_PLAN=1

# Desarrollo: sin plan activo = todo permitido
PERMISSION_REQUIRE_PLAN=0
```

### Requisito: `.env_internal`

`PermissionCache` usa `SecureAuthDB` que necesita las credenciales de core y auth:

```bash
# .env_internal (ya lo tienes configurado)
AUTH_INTERNAL_DSN="mysql:host=localhost;dbname=galappxy_auth;charset=utf8mb4"
AUTH_INTERNAL_USER="auth_limited"
AUTH_INTERNAL_PASS="..."
CORE_INTERNAL_DSN="mysql:host=localhost;dbname=galappxy_core;charset=utf8mb4"
CORE_INTERNAL_USER="core_limited"
CORE_INTERNAL_PASS="..."
```

## Queries agregados al whitelist

### AUTH_ALLOWED_QUERIES (galappxy_auth)

| Query | Descripción |
|-------|-------------|
| `check_api_key_permission` | Verifica un permiso individual de API Key |
| `load_api_key_permissions` | Carga todos los permisos de una API Key (batch) |

### CORE_ALLOWED_QUERIES (galappxy_core)

| Query | Descripción |
|-------|-------------|
| `resolve_profile_role` | Obtiene `id_role` desde `PROFILE` |
| `resolve_permission_id` | Obtiene `id` de `PERMISSION` por código |
| `check_permission_by_role` | Verifica un permiso individual por rol |
| `check_permission_by_profile` | Verifica override de permiso por perfil |
| `check_plan_permission` | Verifica que el plan incluye un permiso |
| `load_role_permissions` | Carga todos los permisos del rol (batch) |
| `load_profile_permission_overrides` | Carga overrides del perfil (batch) |
| `load_plan_permissions` | Carga permisos del plan activo (batch) |

## Respuesta de error 403

```json
{
  "ok": false,
  "error": {
    "code": "FORBIDDEN",
    "message": "No tienes permiso para esta acción",
    "required": ["homework.create"],
    "missing": ["homework.create"]
  }
}
```

## Para aplicar a otros endpoints

Patrón mínimo:

```php
requireAuth();
requirePermission('invoicing.view');
// ... tu lógica
```

No necesitas nada más. `PermissionCache` resuelve todo automáticamente basándose en el JWT/API Key del request actual.
