import { INestApplication } from '@nestjs/common'; import * as bcrypt from 'bcryptjs'; import request from 'supertest'; import { App } from 'supertest/types'; import { PrismaService } from '../src/prisma/prisma.service'; import { createTestApp } from './utils/create-test-app'; import { login } from './utils/api'; /** * A limited "user manager" may look after ordinary accounts but must never * take over - or hand out - an account that can do more than they can. */ describe('User management cannot escalate (e2e)', () => { let app: INestApplication; let prisma: PrismaService; const suffix = `${Date.now()}-${Math.round(Math.random() * 1e6)}`; const password = 'SuperSecret123!'; let managerToken: string; let adminId: string; let beekeeperId: string; let adminRoleId: string; const server = () => app.getHttpServer(); const asManager = (req: request.Test) => req.set('Authorization', `Bearer ${managerToken}`); beforeAll(async () => { app = await createTestApp(); prisma = app.get(PrismaService); const passwordHash = await bcrypt.hash(password, 12); const managerRole = await prisma.role.create({ data: { key: `USER_MANAGER_${suffix}`, name: 'Benutzerpflege', permissions: { create: [ 'users:read', 'users:create', 'users:update', 'users:deactivate', 'users:assign-role', ].map((key) => ({ permission: { connect: { key } } })), }, }, }); await prisma.user.create({ data: { email: `manager-${suffix}@example.com`, passwordHash, name: 'Manager', roleId: managerRole.id, }, }); const admin = await prisma.user.create({ data: { email: `victim-admin-${suffix}@example.com`, passwordHash, name: 'Admin', role: { connect: { key: 'ADMIN' } }, }, }); adminId = admin.id; adminRoleId = admin.roleId; // A second admin, so "last admin" protection is not what blocks us. await prisma.user.create({ data: { email: `other-admin-${suffix}@example.com`, passwordHash, name: 'Admin 2', role: { connect: { key: 'ADMIN' } }, }, }); const narrowRole = await prisma.role.create({ data: { key: `USER_READER_${suffix}`, name: 'Benutzer ansehen', permissions: { create: [{ permission: { connect: { key: 'users:read' } } }], }, }, }); const narrow = await prisma.user.create({ data: { email: `narrow-${suffix}@example.com`, passwordHash, name: 'Leser', roleId: narrowRole.id, }, }); beekeeperId = narrow.id; managerToken = (await login(app, `manager-${suffix}@example.com`, password)) .accessToken; }); afterAll(async () => { await app.close(); }); it('cannot reset the password of a more privileged user', async () => { await asManager( request(server()).post(`/api/v1/users/${adminId}/reset-password`), ) .send({ password: 'Hijacked-Password-1!' }) .expect(403); }); it('cannot reset the MFA of a more privileged user', async () => { await asManager( request(server()).post(`/api/v1/users/${adminId}/mfa/reset`), ).expect(403); }); it('cannot change the email of a more privileged user', async () => { await asManager(request(server()).patch(`/api/v1/users/${adminId}`)) .send({ email: `attacker-${suffix}@example.com` }) .expect(403); }); it('cannot deactivate a more privileged user', async () => { await asManager( request(server()).post(`/api/v1/users/${adminId}/deactivate`), ).expect(403); }); it('cannot hand out a role with more permissions than they hold', async () => { await asManager(request(server()).patch(`/api/v1/users/${beekeeperId}`)) .send({ roleId: adminRoleId }) .expect(403); await asManager(request(server()).post('/api/v1/users')) .send({ email: `new-admin-${suffix}@example.com`, password: 'AnotherSecret123!', name: 'Neuer Admin', roleId: adminRoleId, }) .expect(403); }); it('can still look after a less privileged user', async () => { await asManager( request(server()).post(`/api/v1/users/${beekeeperId}/reset-password`), ) .send({ password: 'Fresh-Password-12!' }) .expect(201); }); });