feat(260911-fh9): getMe/changePassword/adminResetPassword an den Mandanten aus dem Sitzungsnachweis binden
- auth.service.ts: die drei Nach-Anmeldungs-Methoden binden je ueber genau einen Klienten tenantPrisma; adminResetPassword verweigert einem Nicht-SUPER_ADMIN das Kennwort eines SUPER_ADMIN (T-FH9-04); die drei $queryRaw-Anmeldesuchen bleiben unveraendert auf dem ungebundenen Klienten - auth.controller.ts: me/changePassword reichen user.tenantId aus dem Claim durch; adminResetPassword verzweigt ueber resolveTargetTenantId nach Rolle (ADMIN: eigener Mandant; SUPER_ADMIN: gebundener Fan-out UserService.findByIdForPlatformAdmin) — schliesst die Rechteausweitung ueber die Mandantengrenze (T-FH9-01) - auth.module.ts: importiert UserModule, zyklusfrei gemessen - auth.service.spec.ts: Identitaets-Attrappe ersetzt durch zwei unterscheidbare Klienten (__makeBoundClient); 29 Faelle, drei Falsifizierungsnachweise durchgefuehrt und zurueckgenommen Bekannt und erwartet: rls-access-inventory.spec.ts ist nach diesem Commit kurzzeitig rot (Bestandsaufnahme-Zeile auth.service.ts/user zeigt noch "gemischt", gemessen ist jetzt "gebunden") — wird in Aufgabe 3 desselben Plans geschlossen (927/928 Tests gruen, ein bekannter, in Aufgabe 3 behobener Fehlschlag, kein neuer). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
Logger,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Role } from '@prisma/client';
|
||||
import * as argon2 from 'argon2';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { Response } from 'express';
|
||||
@@ -48,6 +50,33 @@ interface AuthLookupResetTokenRow {
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bindung an forTenant() (260911-fh9): dieser Bereich traegt die Grenze
|
||||
* der gesamten Mandantentrennung. `validateUser`, `requestPasswordReset`,
|
||||
* `resetPassword` suchen VOR bekanntem Mandanten — sie bleiben deshalb auf
|
||||
* dem ungebundenen Klienten und laufen ueber die drei
|
||||
* SECURITY-DEFINER-Funktionen aus `20260909160000_auth_lookup_functions`
|
||||
* (Etappe 1, 260909-eor). `getMe`, `changePassword`, `adminResetPassword`
|
||||
* laufen NACH der Anmeldung: der Mandant steht im signierten
|
||||
* Sitzungsnachweis (dem JWT-Claim `tenantId`, das `login()` aus der
|
||||
* Funktionszeile signiert) und wird je Methode ueber GENAU EINEN Klienten
|
||||
* `tenantPrisma` gebunden. Woher der Mandant der drei gebundenen Methoden
|
||||
* kommt: das Claim (`@CurrentUser().tenantId` im Controller) — NICHT die
|
||||
* Anfrageobjekt-Eigenschaft, die `TenantGuard` fuer die oberste Rolle per
|
||||
* Kopfzeile umschaltbar macht (die eigene Zeile liegt immer im eigenen
|
||||
* Mandanten, ein umgeschalteter SUPER_ADMIN muss sich selbst sehen). Fuer
|
||||
* die oberste Rolle bei `adminResetPassword` kommt der Mandant des ZIELS
|
||||
* stattdessen aus dem gebundenen Fan-out `UserService.findByIdForPlatformAdmin`
|
||||
* (Controller-seitig, Praezedenzfall `user.controller.ts` `resolveTargetUser`,
|
||||
* 260910-das).
|
||||
*
|
||||
* Etappe-3-Vorbehalt: sobald Anmeldenamen je Mandant eindeutig werden,
|
||||
* braucht der Anmeldeweg den Mandanten VOR der Suche — ein Umbau der drei
|
||||
* Funktionen (zwei Gleichheitsbedingungen statt einer, ENGER, nicht
|
||||
* weiter), nicht dieser Bereich. Die Bindung der drei Methoden hier haengt
|
||||
* ausschliesslich am Claim `tenantId` und an `User.id` (plattformweite
|
||||
* UUID) und bleibt davon unberuehrt.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly logger = new Logger(AuthService.name);
|
||||
@@ -268,9 +297,17 @@ export class AuthService {
|
||||
* Return enriched profile for the currently authenticated user.
|
||||
* T-gbh-03: Only public fields + isLocalUser/hasAvatar returned — never
|
||||
* passwordHash or ldapDn.
|
||||
*
|
||||
* Bindet an den Mandanten aus dem Sitzungsnachweis (260911-fh9): der
|
||||
* Aufrufer sucht seine EIGENE Zeile, die per Definition im eigenen
|
||||
* Mandanten liegt. Eine fremdmandantige Kennung (kann strukturell nicht
|
||||
* vorkommen, weil der Controller ausschliesslich `user.id` aus dem Claim
|
||||
* durchreicht) liefert unter dem gebundenen Klienten `null`, nicht die
|
||||
* Zeile.
|
||||
*/
|
||||
async getMe(userId: string) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
async getMe(tenantId: string, userId: string) {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const user = await tenantPrisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
@@ -302,14 +339,20 @@ export class AuthService {
|
||||
/**
|
||||
* Change password for the currently logged-in user.
|
||||
* Verifies current password before allowing change.
|
||||
*
|
||||
* Bindet an den Mandanten aus dem Sitzungsnachweis (260911-fh9), EIN
|
||||
* Klient `tenantPrisma` fuer Suche UND Schreiben — dieselbe Begruendung
|
||||
* wie bei getMe() oben: die eigene Zeile liegt im eigenen Mandanten.
|
||||
*/
|
||||
async changePassword(
|
||||
tenantId: string,
|
||||
userId: string,
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
response: Response,
|
||||
): Promise<void> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const user = await tenantPrisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
@@ -323,7 +366,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const passwordHash = await argon2.hash(newPassword);
|
||||
await this.prisma.user.update({
|
||||
await tenantPrisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { passwordHash, mustChangePassword: false },
|
||||
});
|
||||
@@ -350,13 +393,29 @@ export class AuthService {
|
||||
/**
|
||||
* Admin reset of a user's password (D-03 admin reset).
|
||||
* T-02-15: Only ADMIN/SUPER_ADMIN via RolesGuard.
|
||||
*
|
||||
* Bindet an den Mandanten des ZIELS (260911-fh9), EIN Klient
|
||||
* `tenantPrisma`: fuer einen ADMIN-Aufrufer ist das dessen eigener
|
||||
* Mandant aus dem Sitzungsnachweis, fuer SUPER_ADMIN der ueber den
|
||||
* gebundenen Fan-out (Controller, `UserService.findByIdForPlatformAdmin`)
|
||||
* aufgeloeste Mandant des Ziels — beide kommen als `tenantId`-Parameter
|
||||
* bereits fertig aufgeloest hier an. Ein fremdmandantiges Ziel ist unter
|
||||
* dem gebundenen Klienten unsichtbar (T-FH9-01); die
|
||||
* `BadRequestException` nennt weder Halter noch Mandanten. Der Riegel
|
||||
* unten schliesst zusaetzlich die Rechteausweitung INNERHALB des
|
||||
* Mandanten (T-FH9-04): ein Nicht-SUPER_ADMIN darf das Kennwort eines
|
||||
* SUPER_ADMIN nicht setzen. Der Schwesterweg `PATCH /users/:id` hat
|
||||
* dieselbe Luecke nicht geschlossen — offener Ledger-Eintrag T-FH9-05.
|
||||
*/
|
||||
async adminResetPassword(
|
||||
tenantId: string,
|
||||
callerRole: Role,
|
||||
userId: string,
|
||||
newPassword: string,
|
||||
mustChangePassword: boolean = true,
|
||||
): Promise<void> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const user = await tenantPrisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
@@ -364,8 +423,12 @@ export class AuthService {
|
||||
throw new BadRequestException('User not found');
|
||||
}
|
||||
|
||||
if (user.role === Role.SUPER_ADMIN && callerRole !== Role.SUPER_ADMIN) {
|
||||
throw new ForbiddenException('Cannot reset password of a SUPER_ADMIN user');
|
||||
}
|
||||
|
||||
const passwordHash = await argon2.hash(newPassword);
|
||||
await this.prisma.user.update({
|
||||
await tenantPrisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
passwordHash,
|
||||
|
||||
Reference in New Issue
Block a user