feat(quick-260911-nke): Benutzer an 34 Aufrufstellen gesetzt, zehn Tabellen gemessen, sechs Pruefungen umgedreht
- 30 verbleibende forTenant()-Aufrufstellen in sieben Diensten (calendar 6, dashboard 9, favorites 5, tender-email-config 3, tender-notification-pref 2, tender-rss-feed 2, tender-triage 3) reichen userId als drittes Argument durch. tender-digest.scheduler.ts bleibt zweistellig (Hintergrunddienst, Etappe 3c), mit Begruendung im Kommentar. Keine Methodensignatur, kein Controller angefasst, keine anwendungsseitige userId-Filterung entfernt. - rls-scratch-check.mjs: zwoelf Extraktionsstellen auf die neue Migration umgeleitet (TenderEmailConfig/TenderNotificationPref/TenderSavedSearch/ TenderTriage/TenderRssFeedSource in runTendersAreaChecks, SearchProvider in runSearchProviderAreaChecks/runDashboardAreaChecks, DashboardLayout/ WidgetInstance, CalendarSource/FavoriteLink samt regelstand-eindeutig-Gates). SearchProvider/TenderRssFeedSource jetzt mit extractAllPolicySql (4 Regeln). runUserDimensionChecks() um die uebrigen neun Tabellen erweitert (neue Routine runCommandSeparatedPersonalTableCheck fuer die zwei NULL-faehigen Tabellen inkl. gemeinsame-Zeile-Pruefungen). - Sechs Loch-Pruefungen umgedreht (dashboardlayout, widgetinstance, searchprovider, calendarsource, favoritelink-Doppelaussage getrennt) — alte Messung ohne Benutzer bleibt unter neuem Namen, Umkehrung MIT Benutzer erwartet das Gegenteil; kein alter Name mehr als Kennung. - Baseline: 1020/62 Tests weiterhin gruen, Typpruefung sauber, Werkzeug 203/203 bestanden (vorher 146). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -203,6 +203,8 @@ describe('FavoritesService — Bindung an forTenant() (260911-gwh)', () => {
|
||||
|
||||
expect(result.map((r: any) => r.id)).toEqual(['f2', 'f1']);
|
||||
expectBoundCall(prisma, 't1', 'favoriteLink', 'findMany');
|
||||
// Benutzerdimension (260911-nke): forTenant() bekommt userId als drittes Argument.
|
||||
expect(forTenant).toHaveBeenCalledWith(prisma, 't1', 'user-a1');
|
||||
});
|
||||
|
||||
it('liefert unter einem FREMDEN Mandanten eine leere Liste, kein Fehler (der Wert, aus dem das Widget "Noch keine Favoriten." macht)', async () => {
|
||||
|
||||
@@ -23,11 +23,15 @@ import { IconDiscoveryService, normalizeUrl } from './icon-discovery.service';
|
||||
* wuerde Widget und Link unter einem `x-tenant-id`-Wechsel eines
|
||||
* SUPER_ADMIN in verschiedenen Mandanten auseinanderreissen.
|
||||
*
|
||||
* Die Regel auf `FavoriteLink` kennt KEINE Benutzerdimension (260911-gwh,
|
||||
* Aufgabe 1, Pruefung 4 — dieselbe Lehre wie `CalendarSource`/
|
||||
* `DashboardLayout`/`WidgetInstance`) — die `userId`-Filter unten bleiben
|
||||
* deshalb der einzige Schutz gegen Quer-Lesen zwischen Nutzern DESSELBEN
|
||||
* Mandanten (Etappe-3-Entscheidung (2) traegt das nach).
|
||||
* Die Regel auf `FavoriteLink` trug bei der Messung 260911-gwh (Aufgabe 1,
|
||||
* Pruefung 4) KEINE Benutzerdimension — dieselbe Lehre wie `CalendarSource`/
|
||||
* `DashboardLayout`/`WidgetInstance`. Nachtrag (260911-nke, Etappe 3b): seit
|
||||
* Migration 20260911120000 traegt die Regel auf `FavoriteLink` die
|
||||
* Benutzerdimension (`current_user_id() IS NULL OR "userId" = current_user_id()`)
|
||||
* — jeder `forTenant()`-Aufruf unten reicht `userId` als drittes Argument
|
||||
* durch. Die `userId`-Filter unten bleiben trotzdem UNVERAENDERT bestehen:
|
||||
* zweites Netz, kein Ersatz — ein Aufrufer, der `userId` vergisst, saehe
|
||||
* ohne sie den ganzen Mandanten (siehe .planning/WINDOWS.md).
|
||||
*
|
||||
* Access control (T-08-06 / Pitfall 3):
|
||||
* - Every query is scoped by userId (prevents cross-user access).
|
||||
@@ -58,7 +62,7 @@ export class FavoritesService {
|
||||
async list(tenantId: string, userId: string, widgetId: string) {
|
||||
if (!widgetId) throw new BadRequestException('widgetId is required');
|
||||
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
|
||||
return tenantPrisma.favoriteLink.findMany({
|
||||
where: { userId, widgetId },
|
||||
orderBy: [{ position: 'asc' }, { title: 'asc' }],
|
||||
@@ -72,7 +76,7 @@ export class FavoritesService {
|
||||
* If iconUrl is not provided, triggers server-side icon discovery with SSRF protection.
|
||||
*/
|
||||
async create(tenantId: string, userId: string, dto: CreateFavoriteDto) {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
|
||||
|
||||
// T-GWH-05: der Fremdschluessel prueft an der Zeilenschutz-Regel von
|
||||
// WidgetInstance vorbei (Aufgabe 1, Pruefung 7) — ohne diesen Riegel
|
||||
@@ -116,7 +120,7 @@ export class FavoritesService {
|
||||
* Accepts null as an explicit value for iconUrl (clears stored icon).
|
||||
*/
|
||||
async update(tenantId: string, id: string, userId: string, dto: UpdateFavoriteDto) {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
|
||||
const link = await tenantPrisma.favoriteLink.findUnique({ where: { id } });
|
||||
|
||||
if (!link || link.userId !== userId) {
|
||||
@@ -157,7 +161,7 @@ export class FavoritesService {
|
||||
* Verifies userId ownership before deleting (T-08-06).
|
||||
*/
|
||||
async remove(tenantId: string, id: string, userId: string) {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
|
||||
const link = await tenantPrisma.favoriteLink.findUnique({ where: { id } });
|
||||
|
||||
if (!link || link.userId !== userId) {
|
||||
@@ -183,7 +187,7 @@ export class FavoritesService {
|
||||
id: string,
|
||||
userId: string,
|
||||
): Promise<{ contentType: string; body: Buffer }> {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId, userId) as any;
|
||||
const link = await tenantPrisma.favoriteLink.findUnique({ where: { id } });
|
||||
|
||||
if (!link || link.userId !== userId || !link.iconUrl) {
|
||||
|
||||
Reference in New Issue
Block a user