fix(#195): Implement RLS context helpers consistently across all services
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Added workspace context management to PrismaService: - setWorkspaceContext(userId, workspaceId, client?) - Sets session variables - clearWorkspaceContext(client?) - Clears session variables - withWorkspaceContext(userId, workspaceId, fn) - Transaction wrapper Extended db-context.ts with workspace-scoped helpers: - setCurrentWorkspace(workspaceId, client) - setWorkspaceContext(userId, workspaceId, client) - clearWorkspaceContext(client) - withWorkspaceContext(userId, workspaceId, fn) All functions use SET LOCAL for transaction-scoped variables (connection pool safe). Added comprehensive tests (11 passing unit tests). Fixes #195 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@ function getPrismaInstance(): PrismaClient {
|
||||
*
|
||||
* Note: SET LOCAL must be used within a transaction to ensure it's scoped
|
||||
* correctly with connection pooling. This is a low-level function - prefer
|
||||
* using withUserContext or withUserTransaction for most use cases.
|
||||
* using withUserContext or withWorkspaceContext for most use cases.
|
||||
*
|
||||
* @param userId - The UUID of the current user
|
||||
* @param client - Prisma client (required - must be a transaction client)
|
||||
@@ -42,6 +42,53 @@ export async function setCurrentUser(userId: string, client: PrismaClient): Prom
|
||||
await client.$executeRaw`SET LOCAL app.current_user_id = ${userId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current workspace ID for RLS policies within a transaction context.
|
||||
* Must be called before executing any workspace-scoped queries.
|
||||
*
|
||||
* @param workspaceId - The UUID of the current workspace
|
||||
* @param client - Prisma client (required - must be a transaction client)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await prisma.$transaction(async (tx) => {
|
||||
* await setCurrentWorkspace(workspaceId, tx);
|
||||
* const tasks = await tx.task.findMany(); // Filtered by workspace via RLS
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function setCurrentWorkspace(
|
||||
workspaceId: string,
|
||||
client: PrismaClient
|
||||
): Promise<void> {
|
||||
await client.$executeRaw`SET LOCAL app.current_workspace_id = ${workspaceId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets both user and workspace context for RLS policies.
|
||||
* This is the recommended way to set context for workspace-scoped operations.
|
||||
*
|
||||
* @param userId - The UUID of the current user
|
||||
* @param workspaceId - The UUID of the current workspace
|
||||
* @param client - Prisma client (required - must be a transaction client)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await prisma.$transaction(async (tx) => {
|
||||
* await setWorkspaceContext(userId, workspaceId, tx);
|
||||
* const tasks = await tx.task.findMany(); // Filtered by workspace via RLS
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function setWorkspaceContext(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
client: PrismaClient
|
||||
): Promise<void> {
|
||||
await client.$executeRaw`SET LOCAL app.current_user_id = ${userId}`;
|
||||
await client.$executeRaw`SET LOCAL app.current_workspace_id = ${workspaceId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current user context within a transaction.
|
||||
* Use this to reset the session or when switching users.
|
||||
@@ -55,6 +102,19 @@ export async function clearCurrentUser(client: PrismaClient): Promise<void> {
|
||||
await client.$executeRaw`SET LOCAL app.current_user_id = NULL`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears both user and workspace context within a transaction.
|
||||
*
|
||||
* Note: SET LOCAL is automatically cleared at transaction end,
|
||||
* so explicit clearing is typically unnecessary.
|
||||
*
|
||||
* @param client - Prisma client (required - must be a transaction client)
|
||||
*/
|
||||
export async function clearWorkspaceContext(client: PrismaClient): Promise<void> {
|
||||
await client.$executeRaw`SET LOCAL app.current_user_id = NULL`;
|
||||
await client.$executeRaw`SET LOCAL app.current_workspace_id = NULL`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a function with the current user context set within a transaction.
|
||||
* Automatically sets the user context and ensures it's properly scoped.
|
||||
@@ -121,6 +181,36 @@ export async function withUserTransaction<T>(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a function with workspace context (user + workspace) set within a transaction.
|
||||
* This is the recommended way for workspace-scoped operations.
|
||||
*
|
||||
* @param userId - The UUID of the current user
|
||||
* @param workspaceId - The UUID of the current workspace
|
||||
* @param fn - The function to execute with context (receives transaction client)
|
||||
* @returns The result of the function
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const tasks = await withWorkspaceContext(userId, workspaceId, async (tx) => {
|
||||
* return tx.task.findMany({
|
||||
* where: { status: 'IN_PROGRESS' }
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function withWorkspaceContext<T>(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
fn: (tx: PrismaClient) => Promise<T>
|
||||
): Promise<T> {
|
||||
const prismaClient = getPrismaInstance();
|
||||
return prismaClient.$transaction(async (tx) => {
|
||||
await setWorkspaceContext(userId, workspaceId, tx as PrismaClient);
|
||||
return fn(tx as PrismaClient);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Higher-order function that wraps a handler with user context.
|
||||
* Useful for API routes and tRPC procedures.
|
||||
@@ -270,9 +360,13 @@ export function createAuthMiddleware(client: PrismaClient) {
|
||||
|
||||
export default {
|
||||
setCurrentUser,
|
||||
setCurrentWorkspace,
|
||||
setWorkspaceContext,
|
||||
clearCurrentUser,
|
||||
clearWorkspaceContext,
|
||||
withUserContext,
|
||||
withUserTransaction,
|
||||
withWorkspaceContext,
|
||||
withAuth,
|
||||
verifyWorkspaceAccess,
|
||||
getUserWorkspaces,
|
||||
|
||||
Reference in New Issue
Block a user