188 lines
8.5 KiB
Markdown
188 lines
8.5 KiB
Markdown
# Etapa 3 — Enforcement de permisos
|
|
|
|
## Contexto
|
|
|
|
Ro-ut es una plataforma de logística. Las etapas 0-2 están completadas. La Etapa 3 es el siguiente paso en el orden de ejecución: hacer que el campo `permissions` realmente proteja las operaciones GraphQL.
|
|
|
|
**Estado actual:**
|
|
- `userExtractor` (middleware en `src/server/middlewares/userExtractor.ts`) decodifica el JWT y guarda `req.usuario = decoded.sub` (string, ej. `"aleleb"`). Solo valida autenticación, **no verifica permisos**.
|
|
- Los schemas GraphQL (`user.schema.ts`, `company.schema.ts`, `office.schema.ts`) llaman a `userExtractor` en cada resolver, pero nadie lee `req.usuario` ni verifica permisos.
|
|
- `AdminUser` en `user.schema.ts` tiene `ViewUser`, `CreateUser`, `EditUser`, `DeleteUser` como booleanos, pero nunca se consultan.
|
|
- La tabla `User` en PostgreSQL tiene una columna `permissions` (JSONB) que almacena los permisos del usuario.
|
|
- El token JWT del test lleva `sub: "aleleb"` — no lleva permisos embebidos.
|
|
|
|
**Problema:** Cualquiera autenticado puede ejecutar todas las operaciones sin restricciones de permiso.
|
|
|
|
---
|
|
|
|
## Diseño de la solución
|
|
|
|
### Estrategia
|
|
|
|
1. **Crear un helper `checkPermission`** que lea `req.usuario` (username), haga query a la BD para obtener los permisos del usuario, y verifique un permiso específico.
|
|
2. **Actualizar `userExtractor`** para que después de decodificar el JWT, llame al helper y guarde `req.usuario` como un objeto con `{ id, permissions }` en lugar de solo el string.
|
|
3. **Actualizar los schemas** de User, Company y Office para verificar permisos antes de ejecutar cada operación.
|
|
4. **Actualizar los tests** para que verifiquen que los permisos se aplican correctamente.
|
|
|
|
### Patrón de permisos propuesto
|
|
|
|
```
|
|
AdminUser:
|
|
ViewUser: boolean // puede ver usuarios
|
|
CreateUser: boolean // puede crear usuarios
|
|
EditUser: boolean // puede editar usuarios
|
|
DeleteUser: boolean // puede eliminar usuarios
|
|
|
|
AdminCompany: (nuevo)
|
|
ViewCompany: boolean // puede ver empresas
|
|
CreateCompany: boolean // puede crear empresas
|
|
EditCompany: boolean // puede editar empresas
|
|
DeleteCompany: boolean // puede eliminar empresas
|
|
|
|
AdminOffice: (nuevo)
|
|
ViewOffice: boolean // puede ver oficinas
|
|
CreateOffice: boolean // puede crear oficinas
|
|
EditOffice: boolean // puede editar oficinas
|
|
DeleteOffice: boolean // puede eliminar oficinas
|
|
```
|
|
|
|
---
|
|
|
|
## Plan de implementación
|
|
|
|
### Tarea 3.1 — Verificar permisos en controllers de User
|
|
|
|
**Archivos a modificar:**
|
|
|
|
1. **`src/server/middlewares/userExtractor.ts`** — Después de `req.usuario = usuario`, hacer query a la BD para obtener los permisos del usuario y guardar `req.usuario = { id: usuario, permissions: ... }`.
|
|
|
|
2. **`src/server/GraphQL/schema/user.schema.ts`** — En cada query/mutation, verificar el permiso correspondiente antes de ejecutar:
|
|
- `user()` y `users()` → verificar `adminUser.ViewUser`
|
|
- `insertUser()` → verificar `adminUser.CreateUser`
|
|
- `editUser()` → verificar `adminUser.EditUser`
|
|
- `deleteUser()` → verificar `adminUser.DeleteUser`
|
|
|
|
**Detalle del helper `checkPermission`:**
|
|
|
|
```typescript
|
|
// src/server/middlewares/userExtractor.ts (o archivo nuevo checkPermission.ts)
|
|
import db from '@models/apiPostgresModel/config/apiPostgres-connection';
|
|
|
|
export const checkPermission = async (username: string, permissionPath: string): Promise<boolean> => {
|
|
const { rows } = await db.query(
|
|
`SELECT "permissions" FROM "User" WHERE "user" = $1`,
|
|
[username]
|
|
);
|
|
if (!rows.length || !rows[0].permissions) return false;
|
|
|
|
// permissionPath es algo como "adminUser.ViewUser"
|
|
const [namespace, key] = permissionPath.split('.');
|
|
return !!rows[0].permissions[namespace]?.[key];
|
|
};
|
|
```
|
|
|
|
**Detalle de las verificaciones en el schema:**
|
|
|
|
En cada campo async de los ObjectType, antes de llamar al controller:
|
|
|
|
```typescript
|
|
// Ejemplo para query user()
|
|
@Field( () => User, { nullable: true } )
|
|
async user(@Arg('id') id: string, @Ctx() context: { req: any; res: any; }){
|
|
await userExtractor({req: context.req, res: context.res, isGraphQL: true});
|
|
|
|
// Verificar permiso
|
|
const hasPermission = await checkPermission(
|
|
context.req.usuario.id || context.req.usuario,
|
|
'adminUser.ViewUser'
|
|
);
|
|
if (!hasPermission) {
|
|
throw new GraphQLError('No tienes permiso para ver usuarios');
|
|
}
|
|
|
|
return getUserController({ args: { id } });
|
|
}
|
|
```
|
|
|
|
### Tarea 3.2 — Verificar permisos en mutations de User
|
|
|
|
Ya cubierto por el patrón anterior en `user.schema.ts`:
|
|
- `insertUser()` → `adminUser.CreateUser`
|
|
- `editUser()` → `adminUser.EditUser`
|
|
- `deleteUser()` → `adminUser.DeleteUser`
|
|
|
|
### Tarea 3.3 — Definir permisos para Company y Office
|
|
|
|
**Archivos a modificar:**
|
|
|
|
1. **`src/server/GraphQL/schema/company.schema.ts`** — Agregar `AdminCompany` con `ViewCompany`, `CreateCompany`, `EditCompany`, `DeleteCompany`. Verificar en cada campo async.
|
|
|
|
2. **`src/server/GraphQL/schema/office.schema.ts`** — Agregar `AdminOffice` con `ViewOffice`, `CreateOffice`, `EditOffice`, `DeleteOffice`. Verificar en cada campo async.
|
|
|
|
3. **`src/server/GraphQL/schema/user.schema.ts`** — Agregar `AdminCompany` y `AdminOffice` al ObjectType `Permissions` para que el schema GraphQL los exponga.
|
|
|
|
**Detalle de las verificaciones:**
|
|
|
|
Para Company:
|
|
- `company()` y `companies()` → `adminCompany.ViewCompany`
|
|
- `insertCompany()` → `adminCompany.CreateCompany`
|
|
- `editCompany()` → `adminCompany.EditCompany`
|
|
- `deleteCompany()` → `adminCompany.DeleteCompany`
|
|
|
|
Para Office:
|
|
- `office()`, `offices()`, `officesByCompany()` → `adminOffice.ViewOffice`
|
|
- `insertOffice()` → `adminOffice.CreateOffice`
|
|
- `editOffice()` → `adminOffice.EditOffice`
|
|
- `deleteOffice()` → `adminOffice.DeleteOffice`
|
|
|
|
### Tarea 3.4 — Actualizar tests
|
|
|
|
**Archivos a crear/modificar:**
|
|
|
|
1. **`src/server/tests/server/user/index.test.ts`** — Agregar tests que verifiquen:
|
|
- Que un usuario sin `ViewUser` no puede ejecutar `usersQuery`
|
|
- Que un usuario sin `CreateUser` no puede ejecutar `insertUser`
|
|
- Que un usuario sin `EditUser` no puede ejecutar `editUser`
|
|
- Que un usuario sin `DeleteUser` no puede ejecutar `deleteUser`
|
|
|
|
2. **`src/server/tests/server/company/index.test.ts`** — Agregar tests similares para Company.
|
|
|
|
3. **`src/server/tests/server/office/index.test.ts`** — Agregar tests similares para Office.
|
|
|
|
**Nota crítica sobre tests y SQL:**
|
|
|
|
Los tests de integración (`npm run test:backend`) usan `src/server/sql/ro-ut_app_pg.sql` (sin usuario seed) con un token JWT fijo (`sub: "aleleb"`). Los tests E2E usan `src/server/sql/ro-ut_app_pg_with_user.sql` (con usuario seed admin con permisos completos).
|
|
|
|
Para que los tests de integración funcionen con el enforcement de permisos, necesitamos:
|
|
|
|
1. **Agregar un usuario seed a `ro-ut_app_pg.sql`** (o crear un archivo `ro-ut_app_pg_with_test_user.sql` dedicado para integration tests) con un usuario `aleleb` con permisos configurables. El CI workflow en `.gitea/workflows/main-workflow.yml` ejecuta `psql ... -f src/server/sql/ro-ut_app_pg.sql` en el job `integration-back-end-testing`.
|
|
|
|
2. **Crear un helper `createTestToken(username)`** en `src/server/tests/configTest.ts` que genere JWTs con diferentes `sub` claims, para poder probar escenarios de "sin permisos" vs "con permisos".
|
|
|
|
3. **Los tests de permisos denegados** no pueden usar la BD real (porque el usuario seed tendrá permisos). Se necesita:
|
|
- O bien crear un usuario de test con `permissions: {}` vacío
|
|
- O bien mockear la query de permisos en los tests que deben fallar
|
|
|
|
---
|
|
|
|
## Resumen de archivos a modificar
|
|
|
|
| Archivo | Acción | Tarea |
|
|
|---------|--------|-------|
|
|
| `src/server/middlewares/userExtractor.ts` | Modificar | 3.1 - Agregar query de permisos |
|
|
| `src/server/GraphQL/schema/user.schema.ts` | Modificar | 3.1, 3.2, 3.3 - Agregar verificaciones + AdminCompany/AdminOffice |
|
|
| `src/server/GraphQL/schema/company.schema.ts` | Modificar | 3.3 - Agregar verificaciones de permisos |
|
|
| `src/server/GraphQL/schema/office.schema.ts` | Modificar | 3.3 - Agregar verificaciones de permisos |
|
|
| `src/server/tests/server/user/index.test.ts` | Modificar | 3.1, 3.2 - Tests de permisos User |
|
|
| `src/server/tests/server/company/index.test.ts` | Modificar | 3.3 - Tests de permisos Company |
|
|
| `src/server/tests/server/office/index.test.ts` | Modificar | 3.3 - Tests de permisos Office |
|
|
|
|
---
|
|
|
|
## Verificación
|
|
|
|
1. `npm run test:backend` — Todos los tests existentes deben pasar (los nuevos tests verificarán el enforcement).
|
|
2. `npm run lint` — Sin errores de linting.
|
|
3. `npm run build` — Compilación limpia.
|
|
4. Manual: ejecutar un query GraphQL con un token de usuario sin permisos → debe devolver error de permisos.
|