fix: kritische Rechte-Luecke, Admin-Lockout-Schutz und Wetter-Haertung
Dispatchte security-reviewer und code-reviewer Agenten fuer eine vollstaendige Pruefung des RBAC-Systems (d31aa79..HEAD). Ein KRITISCHER Fund, zwei HIGH, ein weiteres MEDIUM, alle behoben: KRITISCH: Deaktivieren eines Benutzers hat dessen Berechtigungen nicht entzogen. PermissionResolverService.loadPermissions() prüfte nur den Rollen-Status, nie User.isActive - ein bereits ausgestelltes Access- Token behielt volle Rechte bis zum Ablauf (bis zu 15 Minuten), nicht die vorgesehenen 30 Sekunden. UsersService rief zudem nie invalidateUser() auf. Beides jetzt behoben; neue dedizierte permission-resolver.service.spec.ts deckt das ab (existierte bisher gar nicht - ein Grund, warum die Luecke unbemerkt blieb). HIGH: Kein Schutz gegen den letzten aktiven Systemrollen-Nutzer (ADMIN). Man konnte den letzten Admin deaktivieren oder umrollen und sich damit komplett aussperren - ohne Selbsthilfe-Weg, da prisma/seed.ts das Seeden ueberspringt, sobald die Admin-E-Mail bereits existiert. UsersService.deactivate()/.update() pruefen jetzt per assertNotLastSystemRoleUser(), analog zum bestehenden Lockout-Schutz in RolesService fuer Systemrollen selbst. HIGH: Der Open-Meteo-Aufruf in WeatherService hatte kein Timeout und konnte unbegrenzt haengen. Jetzt mit AbortSignal.timeout(8000) plus sauberem Fehler-Mapping. MEDIUM: Trachtprognose fiel bei fehlenden Wetterdaten stillschweigend auf "MAESSIG" zurueck statt einen eigenen Unbekannt-Zustand zu zeigen - ein Imker haette eine echte Einschaetzung gesehen, wo keine moeglich war. Neuer Rating-Wert UNBEKANNT in Backend und Frontend. LOW: Veralteter Kommentar an PermissionsGuard, der noch den in Phase 3 geloeschten RolesGuard erwaehnte, korrigiert. Verifiziert: tsc, lint, build auf beiden Projekten gruen. Backend-Unit 245/245 (17 neue Tests fuer die Fixes). Frontend-Unit 14/14. Volle Backend-e2e-Suite konnte weiterhin nicht laufen (WSL/Postgres von Windows aus nicht erreichbar, auch nach vollem "wsl --shutdown"- Neustart) - jeder Fix hat aber gezielte, gruene Unit-Abdeckung. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
f1d6b2956e
commit
5a1015c4a0
9 files changed
+329
-11
No files matched your search
@@ -0,0 +1,134 @@
|
||||
import { PermissionResolverService } from './permission-resolver.service';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
describe('PermissionResolverService', () => {
|
||||
let resolver: PermissionResolverService;
|
||||
let prisma: { user: { findUnique: jest.Mock } };
|
||||
|
||||
function activeUserWithPermissions(...keys: string[]) {
|
||||
return {
|
||||
isActive: true,
|
||||
role: {
|
||||
status: 'ACTIVE',
|
||||
permissions: keys.map((key) => ({ permission: { key } })),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
prisma = { user: { findUnique: jest.fn() } };
|
||||
resolver = new PermissionResolverService(
|
||||
prisma as unknown as PrismaService,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the granted permission keys for an active user with an active role', async () => {
|
||||
prisma.user.findUnique.mockResolvedValue(
|
||||
activeUserWithPermissions('colonies:read', 'tasks:create'),
|
||||
);
|
||||
|
||||
const permissions = await resolver.getPermissions('user-1');
|
||||
|
||||
expect(Array.from(permissions).sort()).toEqual(
|
||||
['colonies:read', 'tasks:create'].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns no permissions for a deactivated user, even if their role is active', async () => {
|
||||
prisma.user.findUnique.mockResolvedValue({
|
||||
isActive: false,
|
||||
role: {
|
||||
status: 'ACTIVE',
|
||||
permissions: [{ permission: { key: 'colonies:read' } }],
|
||||
},
|
||||
});
|
||||
|
||||
const permissions = await resolver.getPermissions('user-1');
|
||||
|
||||
expect(permissions.size).toBe(0);
|
||||
});
|
||||
|
||||
it('returns no permissions when the role itself is deactivated', async () => {
|
||||
prisma.user.findUnique.mockResolvedValue({
|
||||
isActive: true,
|
||||
role: {
|
||||
status: 'INACTIVE',
|
||||
permissions: [{ permission: { key: 'colonies:read' } }],
|
||||
},
|
||||
});
|
||||
|
||||
const permissions = await resolver.getPermissions('user-1');
|
||||
|
||||
expect(permissions.size).toBe(0);
|
||||
});
|
||||
|
||||
it('returns no permissions when the user does not exist', async () => {
|
||||
prisma.user.findUnique.mockResolvedValue(null);
|
||||
|
||||
const permissions = await resolver.getPermissions('missing');
|
||||
|
||||
expect(permissions.size).toBe(0);
|
||||
});
|
||||
|
||||
it('caches the result and does not re-query within the TTL', async () => {
|
||||
prisma.user.findUnique.mockResolvedValue(
|
||||
activeUserWithPermissions('colonies:read'),
|
||||
);
|
||||
|
||||
await resolver.getPermissions('user-1');
|
||||
await resolver.getPermissions('user-1');
|
||||
|
||||
expect(prisma.user.findUnique).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('re-queries after invalidateUser', async () => {
|
||||
prisma.user.findUnique.mockResolvedValue(
|
||||
activeUserWithPermissions('colonies:read'),
|
||||
);
|
||||
|
||||
await resolver.getPermissions('user-1');
|
||||
resolver.invalidateUser('user-1');
|
||||
await resolver.getPermissions('user-1');
|
||||
|
||||
expect(prisma.user.findUnique).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('re-queries after invalidateAll', async () => {
|
||||
prisma.user.findUnique.mockResolvedValue(
|
||||
activeUserWithPermissions('colonies:read'),
|
||||
);
|
||||
|
||||
await resolver.getPermissions('user-1');
|
||||
resolver.invalidateAll();
|
||||
await resolver.getPermissions('user-1');
|
||||
|
||||
expect(prisma.user.findUnique).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
describe('hasAny', () => {
|
||||
it('returns true when the user holds at least one of the requested keys', async () => {
|
||||
prisma.user.findUnique.mockResolvedValue(
|
||||
activeUserWithPermissions('colonies:read'),
|
||||
);
|
||||
|
||||
await expect(
|
||||
resolver.hasAny('user-1', ['tasks:create', 'colonies:read']),
|
||||
).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the user holds none of the requested keys', async () => {
|
||||
prisma.user.findUnique.mockResolvedValue(
|
||||
activeUserWithPermissions('colonies:read'),
|
||||
);
|
||||
|
||||
await expect(resolver.hasAny('user-1', ['tasks:create'])).resolves.toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns true when no keys are required', async () => {
|
||||
await expect(resolver.hasAny('user-1', [])).resolves.toBe(true);
|
||||
expect(prisma.user.findUnique).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user