8.5 KiB
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 ensrc/server/middlewares/userExtractor.ts) decodifica el JWT y guardareq.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 auserExtractoren cada resolver, pero nadie leereq.usuarioni verifica permisos. AdminUserenuser.schema.tstieneViewUser,CreateUser,EditUser,DeleteUsercomo booleanos, pero nunca se consultan.- La tabla
Useren PostgreSQL tiene una columnapermissions(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
- Crear un helper
checkPermissionque leareq.usuario(username), haga query a la BD para obtener los permisos del usuario, y verifique un permiso específico. - Actualizar
userExtractorpara que después de decodificar el JWT, llame al helper y guardereq.usuariocomo un objeto con{ id, permissions }en lugar de solo el string. - Actualizar los schemas de User, Company y Office para verificar permisos antes de ejecutar cada operación.
- 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:
-
src/server/middlewares/userExtractor.ts— Después dereq.usuario = usuario, hacer query a la BD para obtener los permisos del usuario y guardarreq.usuario = { id: usuario, permissions: ... }. -
src/server/GraphQL/schema/user.schema.ts— En cada query/mutation, verificar el permiso correspondiente antes de ejecutar:user()yusers()→ verificaradminUser.ViewUserinsertUser()→ verificaradminUser.CreateUsereditUser()→ verificaradminUser.EditUserdeleteUser()→ verificaradminUser.DeleteUser
Detalle del helper checkPermission:
// 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:
// 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.CreateUsereditUser()→adminUser.EditUserdeleteUser()→adminUser.DeleteUser
Tarea 3.3 — Definir permisos para Company y Office
Archivos a modificar:
-
src/server/GraphQL/schema/company.schema.ts— AgregarAdminCompanyconViewCompany,CreateCompany,EditCompany,DeleteCompany. Verificar en cada campo async. -
src/server/GraphQL/schema/office.schema.ts— AgregarAdminOfficeconViewOffice,CreateOffice,EditOffice,DeleteOffice. Verificar en cada campo async. -
src/server/GraphQL/schema/user.schema.ts— AgregarAdminCompanyyAdminOfficeal ObjectTypePermissionspara que el schema GraphQL los exponga.
Detalle de las verificaciones:
Para Company:
company()ycompanies()→adminCompany.ViewCompanyinsertCompany()→adminCompany.CreateCompanyeditCompany()→adminCompany.EditCompanydeleteCompany()→adminCompany.DeleteCompany
Para Office:
office(),offices(),officesByCompany()→adminOffice.ViewOfficeinsertOffice()→adminOffice.CreateOfficeeditOffice()→adminOffice.EditOfficedeleteOffice()→adminOffice.DeleteOffice
Tarea 3.4 — Actualizar tests
Archivos a crear/modificar:
-
src/server/tests/server/user/index.test.ts— Agregar tests que verifiquen:- Que un usuario sin
ViewUserno puede ejecutarusersQuery - Que un usuario sin
CreateUserno puede ejecutarinsertUser - Que un usuario sin
EditUserno puede ejecutareditUser - Que un usuario sin
DeleteUserno puede ejecutardeleteUser
- Que un usuario sin
-
src/server/tests/server/company/index.test.ts— Agregar tests similares para Company. -
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:
-
Agregar un usuario seed a
ro-ut_app_pg.sql(o crear un archivoro-ut_app_pg_with_test_user.sqldedicado para integration tests) con un usuarioalelebcon permisos configurables. El CI workflow en.gitea/workflows/main-workflow.ymlejecutapsql ... -f src/server/sql/ro-ut_app_pg.sqlen el jobintegration-back-end-testing. -
Crear un helper
createTestToken(username)ensrc/server/tests/configTest.tsque genere JWTs con diferentessubclaims, para poder probar escenarios de "sin permisos" vs "con permisos". -
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
- O bien crear un usuario de test con
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
npm run test:backend— Todos los tests existentes deben pasar (los nuevos tests verificarán el enforcement).npm run lint— Sin errores de linting.npm run build— Compilación limpia.- Manual: ejecutar un query GraphQL con un token de usuario sin permisos → debe devolver error de permisos.