From 751391738630c45681e5c52db9d88dc8fcc58745 Mon Sep 17 00:00:00 2001 From: Fred <98240+farnoux@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:04:28 +0100 Subject: [PATCH] =?UTF-8?q?Les=20transactions=20Drizzle=20supportent=20l'a?= =?UTF-8?q?uthentification=20Supabase=20et=20donc=20les=20RLS=20?= =?UTF-8?q?=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permet notamment de gérer la bonne mise à jour du champ `modified_by` de la table `fiche_action`. --- backend/src/auth/models/auth.models.ts | 6 ++- .../src/common/services/database.service.ts | 52 ++++++++++++++++--- .../services/fiches-action-update.service.ts | 17 +++--- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/backend/src/auth/models/auth.models.ts b/backend/src/auth/models/auth.models.ts index 3c2b8788e1..9ff2cada30 100644 --- a/backend/src/auth/models/auth.models.ts +++ b/backend/src/auth/models/auth.models.ts @@ -6,12 +6,11 @@ export enum AuthRole { ANON = 'anon', // Anonymous } -// export type User = Pick; - export interface AuthUser { id: Role extends AuthRole.AUTHENTICATED ? string : null; role: Role; isAnonymous: Role extends AuthRole.AUTHENTICATED ? false : true; + jwtToken: AuthJwtPayload; } export type AnonymousUser = AuthUser; @@ -49,6 +48,7 @@ export function jwtToUser(jwt: AuthJwtPayload): AuthUser { id: jwt.sub, role: AuthRole.AUTHENTICATED, isAnonymous: false, + jwtToken: jwt, }; } @@ -57,6 +57,7 @@ export function jwtToUser(jwt: AuthJwtPayload): AuthUser { id: null, role: AuthRole.ANON, isAnonymous: true, + jwtToken: jwt, }; } @@ -65,6 +66,7 @@ export function jwtToUser(jwt: AuthJwtPayload): AuthUser { id: null, role: AuthRole.SERVICE_ROLE, isAnonymous: true, + jwtToken: jwt, }; } diff --git a/backend/src/common/services/database.service.ts b/backend/src/common/services/database.service.ts index cfee8d3ff8..591ba41e59 100644 --- a/backend/src/common/services/database.service.ts +++ b/backend/src/common/services/database.service.ts @@ -1,24 +1,60 @@ +import { AuthUser } from '@/backend/auth/models/auth.models'; import { Injectable, Logger, OnApplicationShutdown } from '@nestjs/common'; -import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js'; +import { sql } from 'drizzle-orm'; +import { drizzle } from 'drizzle-orm/postgres-js'; import { default as postgres } from 'postgres'; import ConfigurationService from '../../config/configuration.service'; @Injectable() export default class DatabaseService implements OnApplicationShutdown { private readonly logger = new Logger(DatabaseService.name); - public readonly db: PostgresJsDatabase; - private readonly client: postgres.Sql; - constructor(private readonly configService: ConfigurationService) { - this.logger.log(`Initializing database service`); - this.client = postgres(this.configService.get('SUPABASE_DATABASE_URL'), { + private readonly client = postgres( + this.configService.get('SUPABASE_DATABASE_URL'), + { prepare: false, connection: { application_name: `Backend ${process.env.APPLICATION_VERSION}`, }, - }); + } + ); + + public readonly db = drizzle(this.client); + + constructor(private readonly configService: ConfigurationService) { + this.logger.log(`Initializing database service`); + } - this.db = drizzle(this.client); + // Taken from https://orm.drizzle.team/docs/rls + rls(user: AuthUser) { + return (async (transaction, ...rest) => { + return await this.db.transaction(async (tx) => { + // Supabase exposes auth.uid() and auth.jwt() + // https://supabase.com/docs/guides/database/postgres/row-level-security#helper-functions + try { + await tx.execute(sql` + -- auth.jwt() + select set_config('request.jwt.claims', '${sql.raw( + JSON.stringify(user.jwtToken) + )}', TRUE); + -- auth.uid() + select set_config('request.jwt.claim.sub', '${sql.raw( + user.jwtToken.sub ?? '' + )}', TRUE); + -- set local role + set local role ${sql.raw(user.jwtToken.role ?? 'anon')}; + `); + return await transaction(tx); + } finally { + await tx.execute(sql` + -- reset + select set_config('request.jwt.claims', NULL, TRUE); + select set_config('request.jwt.claim.sub', NULL, TRUE); + reset role; + `); + } + }, ...rest); + }) as typeof this.db.transaction; } async onApplicationShutdown(signal: string) { diff --git a/backend/src/fiches/services/fiches-action-update.service.ts b/backend/src/fiches/services/fiches-action-update.service.ts index 70947135eb..77ac123650 100644 --- a/backend/src/fiches/services/fiches-action-update.service.ts +++ b/backend/src/fiches/services/fiches-action-update.service.ts @@ -77,9 +77,9 @@ export default class FichesActionUpdateService { async updateFicheAction( ficheActionId: number, body: UpdateFicheActionRequestType, - tokenInfo: AuthenticatedUser + user: AuthenticatedUser ) { - await this.ficheService.canWriteFiche(ficheActionId, tokenInfo); + await this.ficheService.canWriteFiche(ficheActionId, user); this.logger.log( `Mise à jour de la fiche action dont l'id est ${ficheActionId}` @@ -103,7 +103,7 @@ export default class FichesActionUpdateService { ...unsafeFicheAction } = body; - return await this.databaseService.db.transaction(async (tx) => { + return await this.databaseService.rls(user)(async (tx) => { const existingFicheAction = await this.databaseService.db .select() .from(ficheActionTable) @@ -136,10 +136,15 @@ export default class FichesActionUpdateService { * Updates fiche action properties */ - if (Object.keys(ficheAction).length > 0) { + if (Object.keys(body).length > 0) { updatedFicheAction = await tx .update(ficheActionTable) - .set(ficheAction) + // `modifiedBy` in the fiche_action table is automatically updated + // via a PG trigger (using the current authenticated user of `db.rls()`). + // But we also set it here so that when the updating object `ficheAction` is empty, + // the trigger can be well… triggered (corresponding to updates only affecting + // related tables, and not directly the fiche_action main table) + .set({ ...ficheAction, modifiedBy: user.id }) .where(eq(ficheActionTable.id, ficheActionId)) .returning(); } @@ -335,7 +340,7 @@ export default class FichesActionUpdateService { libresTag.map((relation) => ({ ficheId: ficheActionId, libreTagId: relation.id, - createdBy: tokenInfo.id, + createdBy: user.id, })) ) .returning();