mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 19:14:03 +00:00
feat(db): enforce runtime RLS boundaries
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { and, desc, eq, gte, inArray, isNotNull, isNull, lt, lte, or, sql } from 'drizzle-orm'
|
||||
|
||||
import { createDbClient, schema } from '@household/db'
|
||||
import { createDbClient, type DbSessionContext, schema } from '@household/db'
|
||||
import type { FinanceRepository } from '@household/ports'
|
||||
import {
|
||||
instantFromDatabaseValue,
|
||||
@@ -22,14 +22,22 @@ function toCurrencyCode(raw: string): CurrencyCode {
|
||||
|
||||
export function createDbFinanceRepository(
|
||||
databaseUrl: string,
|
||||
householdId: string
|
||||
householdId: string,
|
||||
options: {
|
||||
sessionContext?: DbSessionContext
|
||||
} = {}
|
||||
): {
|
||||
repository: FinanceRepository
|
||||
close: () => Promise<void>
|
||||
} {
|
||||
const { db, queryClient } = createDbClient(databaseUrl, {
|
||||
max: 5,
|
||||
prepare: false
|
||||
prepare: false,
|
||||
...(options.sessionContext
|
||||
? {
|
||||
sessionContext: options.sessionContext
|
||||
}
|
||||
: {})
|
||||
})
|
||||
|
||||
async function loadPurchaseParticipants(purchaseIds: readonly string[]): Promise<
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { and, asc, eq, sql } from 'drizzle-orm'
|
||||
|
||||
import { createDbClient, schema } from '@household/db'
|
||||
import { createDbClient, type DbSessionContext, schema } from '@household/db'
|
||||
import {
|
||||
instantToDate,
|
||||
normalizeSupportedLocale,
|
||||
@@ -334,13 +334,23 @@ function utilityCategorySlug(name: string): string {
|
||||
.slice(0, 48)
|
||||
}
|
||||
|
||||
export function createDbHouseholdConfigurationRepository(databaseUrl: string): {
|
||||
export function createDbHouseholdConfigurationRepository(
|
||||
databaseUrl: string,
|
||||
options: {
|
||||
sessionContext?: DbSessionContext
|
||||
} = {}
|
||||
): {
|
||||
repository: HouseholdConfigurationRepository
|
||||
close: () => Promise<void>
|
||||
} {
|
||||
const { db, queryClient } = createDbClient(databaseUrl, {
|
||||
max: 5,
|
||||
prepare: false
|
||||
prepare: false,
|
||||
...(options.sessionContext
|
||||
? {
|
||||
sessionContext: options.sessionContext
|
||||
}
|
||||
: {})
|
||||
})
|
||||
|
||||
const defaultUtilityCategories = [
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"0019_faithful_madame_masque.sql": "38711341799b04a7c47fcc64fd19faf5b26e6f183d6a4c01d492b9929cd63641",
|
||||
"0020_natural_mauler.sql": "a80a4a0196a3b4931040850089346d1bc99b34a5afca77d6d62478ee4b8902c1",
|
||||
"0020_silver_payments.sql": "9686235c75453f1eaa016f2f4ab7fce8fe964c76a4e3515987a2b9f90bd7b1ad",
|
||||
"0021_sharp_payer.sql": "973596e154382984ba7769979ea58298b6d93c5139540854be01e8b283ddb4f1"
|
||||
"0021_sharp_payer.sql": "973596e154382984ba7769979ea58298b6d93c5139540854be01e8b283ddb4f1",
|
||||
"0022_harden_rls.sql": "d2e24b3e5b7ec7ef9da7e90c0ddf0e408764f3578af3872f76b9b3198ffbd70e"
|
||||
}
|
||||
}
|
||||
|
||||
1225
packages/db/drizzle/0022_harden_rls.sql
Normal file
1225
packages/db/drizzle/0022_harden_rls.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -155,6 +155,13 @@
|
||||
"when": 1774200000000,
|
||||
"tag": "0021_sharp_payer",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 22,
|
||||
"version": "7",
|
||||
"when": 1774204831000,
|
||||
"tag": "0022_harden_rls",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
import postgres from 'postgres'
|
||||
import { drizzle } from 'drizzle-orm/postgres-js'
|
||||
|
||||
export interface DbSessionContext {
|
||||
telegramUserId?: string
|
||||
householdId?: string
|
||||
memberId?: string
|
||||
isAdmin?: boolean
|
||||
isWorker?: boolean
|
||||
}
|
||||
|
||||
export interface DbClientOptions {
|
||||
max?: number
|
||||
prepare?: boolean
|
||||
sessionContext?: DbSessionContext
|
||||
}
|
||||
|
||||
function quoteRuntimeOptionValue(value: string): string {
|
||||
return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`
|
||||
}
|
||||
|
||||
function appendRuntimeOption(
|
||||
options: string[],
|
||||
key: string,
|
||||
value: string | boolean | undefined
|
||||
): void {
|
||||
if (value === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
options.push(`-c ${key}=${quoteRuntimeOptionValue(String(value))}`)
|
||||
}
|
||||
|
||||
export function createDbClient(databaseUrl: string, options: DbClientOptions = {}) {
|
||||
@@ -17,7 +42,17 @@ export function createDbClient(databaseUrl: string, options: DbClientOptions = {
|
||||
url.searchParams.delete('options')
|
||||
|
||||
// Set search_path via options parameter (required for PgBouncer compatibility)
|
||||
url.searchParams.set('options', `-c search_path=${dbSchema}`)
|
||||
const runtimeOptions = [`-c search_path=${dbSchema}`]
|
||||
appendRuntimeOption(
|
||||
runtimeOptions,
|
||||
'app.telegram_user_id',
|
||||
options.sessionContext?.telegramUserId
|
||||
)
|
||||
appendRuntimeOption(runtimeOptions, 'app.household_id', options.sessionContext?.householdId)
|
||||
appendRuntimeOption(runtimeOptions, 'app.member_id', options.sessionContext?.memberId)
|
||||
appendRuntimeOption(runtimeOptions, 'app.is_admin', options.sessionContext?.isAdmin)
|
||||
appendRuntimeOption(runtimeOptions, 'app.is_worker', options.sessionContext?.isWorker)
|
||||
url.searchParams.set('options', runtimeOptions.join(' '))
|
||||
|
||||
const cleanUrl = url.toString()
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { createDbClient } from './client'
|
||||
export type { DbSessionContext } from './client'
|
||||
export * as schema from './schema'
|
||||
|
||||
Reference in New Issue
Block a user