0b34e82b21
JwtStrategy.validate las bisher alles aus dem 30-Tage-Token: ein herabgestufter Administrator behielt seine Rechte bis zum Ablauf, ein deaktiviertes oder geloeschtes Konto arbeitete mit seiner Sitzung weiter, und Oberflaeche (/auth/me aus der DB) und API (Token) sahen verschiedene Rollen – die Benutzerliste scheiterte nach einer Rollenaenderung (Befund des Nutzers auf alpha). Jetzt ein gebundener PK-Lesezugriff je Anfrage (forTenant), 401 bei fehlendem, deaktiviertem oder mandantenfremdem Konto. Lokal nachgewiesen: Herabstufen -> sofort 403, Deaktivieren -> sofort 401. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
import { UnauthorizedException } from '@nestjs/common';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import { forTenant } from '../../prisma/prisma-tenant.extension';
|
|
import { JwtStrategy } from './jwt.strategy';
|
|
|
|
vi.mock('../../prisma/prisma-tenant.extension', () => ({
|
|
forTenant: vi.fn((p: unknown) => p),
|
|
}));
|
|
|
|
/**
|
|
* JwtStrategy.validate — seit quick-260930 kommen Rolle, Aktiv-Status und
|
|
* Kennwort-Pflicht bei jeder Anfrage aus der Datenbank, nicht aus dem Token
|
|
* (Rollenaenderung/Deaktivierung wirkt sofort). Direkte Konstruktion ohne
|
|
* Nest-Testmodul, Muster aus `../../tenant/tenant.guard.spec.ts`.
|
|
*/
|
|
|
|
function makeConfigService() {
|
|
return { get: () => 'test-secret' } as any;
|
|
}
|
|
|
|
type Row = {
|
|
id: string;
|
|
username: string;
|
|
role: string;
|
|
tenantId: string;
|
|
isActive: boolean;
|
|
mustChangePassword: boolean;
|
|
} | null;
|
|
|
|
function makePrisma(row: Row) {
|
|
return { user: { findUnique: vi.fn(async () => row) } } as any;
|
|
}
|
|
|
|
const payload = {
|
|
sub: 'u1',
|
|
username: 'kschaller',
|
|
role: 'SUPER_ADMIN' as const,
|
|
tenantId: 't1',
|
|
mustChangePassword: false,
|
|
};
|
|
|
|
const dbRow = {
|
|
id: 'u1',
|
|
username: 'kschaller',
|
|
role: 'ADMIN',
|
|
tenantId: 't1',
|
|
isActive: true,
|
|
mustChangePassword: false,
|
|
};
|
|
|
|
describe('JwtStrategy.validate', () => {
|
|
it('Rolle kommt aus der Datenbank, nicht aus dem Token (herabgestufter Super-Admin ist sofort Admin)', async () => {
|
|
const prisma = makePrisma(dbRow);
|
|
const strategy = new JwtStrategy(makeConfigService(), prisma);
|
|
|
|
const result = await strategy.validate(payload);
|
|
|
|
expect(result).toEqual({
|
|
id: 'u1',
|
|
username: 'kschaller',
|
|
role: 'ADMIN',
|
|
tenantId: 't1',
|
|
mustChangePassword: false,
|
|
});
|
|
expect(forTenant).toHaveBeenCalledWith(prisma, 't1');
|
|
expect(prisma.user.findUnique).toHaveBeenCalledWith(
|
|
expect.objectContaining({ where: { id: 'u1' } }),
|
|
);
|
|
});
|
|
|
|
it('deaktiviertes Konto: 401, auch mit gueltigem Token', async () => {
|
|
const strategy = new JwtStrategy(makeConfigService(), makePrisma({ ...dbRow, isActive: false }));
|
|
await expect(strategy.validate(payload)).rejects.toBeInstanceOf(UnauthorizedException);
|
|
});
|
|
|
|
it('geloeschtes Konto: 401', async () => {
|
|
const strategy = new JwtStrategy(makeConfigService(), makePrisma(null));
|
|
await expect(strategy.validate(payload)).rejects.toBeInstanceOf(UnauthorizedException);
|
|
});
|
|
|
|
it('Konto gehoert nicht (mehr) zum Mandanten aus dem Token: 401', async () => {
|
|
const strategy = new JwtStrategy(makeConfigService(), makePrisma({ ...dbRow, tenantId: 't2' }));
|
|
await expect(strategy.validate(payload)).rejects.toBeInstanceOf(UnauthorizedException);
|
|
});
|
|
|
|
it('Kennwort-Pflicht kommt aus der Datenbank (vom Administrator nachtraeglich gesetzt)', async () => {
|
|
const strategy = new JwtStrategy(
|
|
makeConfigService(),
|
|
makePrisma({ ...dbRow, mustChangePassword: true }),
|
|
);
|
|
const result = await strategy.validate(payload);
|
|
expect(result.mustChangePassword).toBe(true);
|
|
});
|
|
});
|