diff --git a/packages/runtime/src/enhancements/omit.ts b/packages/runtime/src/enhancements/omit.ts index 8f12f540e..2b3f455c9 100644 --- a/packages/runtime/src/enhancements/omit.ts +++ b/packages/runtime/src/enhancements/omit.ts @@ -7,11 +7,21 @@ import { DefaultPrismaProxyHandler, makeProxy } from './proxy'; import { ModelMeta } from './types'; import { enumerate, getModelFields } from './utils'; +/** + * Options for @see withOmit + */ +export type WithOmitOptions = { + /** + * Model metatadata + */ + modelMeta?: ModelMeta; +}; + /** * Gets an enhanced Prisma client that supports @omit attribute. */ -export function withOmit(prisma: DbClient, modelMeta?: ModelMeta): DbClient { - const _modelMeta = modelMeta ?? getDefaultModelMeta(); +export function withOmit(prisma: DbClient, options?: WithOmitOptions): DbClient { + const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(); return makeProxy( prisma, _modelMeta, diff --git a/packages/runtime/src/enhancements/password.ts b/packages/runtime/src/enhancements/password.ts index 2688cb965..e6cb513df 100644 --- a/packages/runtime/src/enhancements/password.ts +++ b/packages/runtime/src/enhancements/password.ts @@ -9,11 +9,21 @@ import { NestedWriteVisitor } from './nested-write-vistor'; import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy'; import { ModelMeta } from './types'; +/** + * Options for @see withPassword + */ +export type WithPasswordOptions = { + /** + * Model metatadata + */ + modelMeta?: ModelMeta; +}; + /** * Gets an enhanced Prisma client that supports @password attribute. */ -export function withPassword(prisma: DbClient, modelMeta?: ModelMeta): DbClient { - const _modelMeta = modelMeta ?? getDefaultModelMeta(); +export function withPassword(prisma: DbClient, options?: WithPasswordOptions): DbClient { + const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(); return makeProxy( prisma, _modelMeta, diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index b497f53e2..1c40e055e 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -4,7 +4,7 @@ import { CrudFailureReason } from '../../constants'; import { AuthUser, DbClientContract, PolicyOperationKind } from '../../types'; import { BatchResult, PrismaProxyHandler } from '../proxy'; import { ModelMeta, PolicyDef } from '../types'; -import { prismaClientValidationError } from '../utils'; +import { formatObject, prismaClientValidationError } from '../utils'; import { Logger } from './logger'; import { PolicyUtil } from './policy-utils'; @@ -20,10 +20,11 @@ export class PolicyProxyHandler implements Pr private readonly policy: PolicyDef, private readonly modelMeta: ModelMeta, private readonly model: string, - private readonly user?: AuthUser + private readonly user?: AuthUser, + private readonly logPrismaQuery?: boolean ) { this.logger = new Logger(prisma); - this.utils = new PolicyUtil(this.prisma, this.modelMeta, this.policy, this.user); + this.utils = new PolicyUtil(this.prisma, this.modelMeta, this.policy, this.user, this.logPrismaQuery); } private get modelClient() { @@ -107,9 +108,12 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if the created // entity fails access policies - const result: any = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => - dbOps.create(writeArgs) - ); + const result: any = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => { + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`create\`: ${formatObject(writeArgs)}`); + } + return dbOps.create(writeArgs); + }); const ids = this.utils.getEntityIds(this.model, result); if (Object.keys(ids).length === 0) { @@ -133,9 +137,12 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if any created // entity fails access policies - const result = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => - dbOps.createMany(writeArgs, skipDuplicates) - ); + const result = await this.utils.processWrite(this.model, 'create', args, (dbOps, writeArgs) => { + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`createMany\`: ${formatObject(writeArgs)}`); + } + return dbOps.createMany(writeArgs, skipDuplicates); + }); return result as BatchResult; } @@ -158,9 +165,12 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if any nested // create fails access policies - const result: any = await this.utils.processWrite(this.model, 'update', args, (dbOps, writeArgs) => - dbOps.update(writeArgs) - ); + const result: any = await this.utils.processWrite(this.model, 'update', args, (dbOps, writeArgs) => { + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`update\`: ${formatObject(writeArgs)}`); + } + return dbOps.update(writeArgs); + }); const ids = this.utils.getEntityIds(this.model, result); if (Object.keys(ids).length === 0) { @@ -183,9 +193,12 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if any nested // create fails access policies - const result = await this.utils.processWrite(this.model, 'updateMany', args, (dbOps, writeArgs) => - dbOps.updateMany(writeArgs) - ); + const result = await this.utils.processWrite(this.model, 'updateMany', args, (dbOps, writeArgs) => { + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`updateMany\`: ${formatObject(writeArgs)}`); + } + return dbOps.updateMany(writeArgs); + }); return result as BatchResult; } @@ -212,9 +225,12 @@ export class PolicyProxyHandler implements Pr // use a transaction to wrap the write so it can be reverted if any nested // create fails access policies - const result: any = await this.utils.processWrite(this.model, 'upsert', args, (dbOps, writeArgs) => - dbOps.upsert(writeArgs) - ); + const result: any = await this.utils.processWrite(this.model, 'upsert', args, (dbOps, writeArgs) => { + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`upsert\`: ${formatObject(writeArgs)}`); + } + return dbOps.upsert(writeArgs); + }); const ids = this.utils.getEntityIds(this.model, result); if (Object.keys(ids).length === 0) { @@ -248,6 +264,9 @@ export class PolicyProxyHandler implements Pr } // conduct the deletion + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`delete\`:\n${formatObject(args)}`); + } await this.modelClient.delete(args); if (!readResult) { @@ -270,6 +289,9 @@ export class PolicyProxyHandler implements Pr await this.utils.injectAuthGuard(args, this.model, 'delete'); // conduct the deletion + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`deleteMany\`:\n${formatObject(args)}`); + } return this.modelClient.deleteMany(args); } @@ -282,6 +304,10 @@ export class PolicyProxyHandler implements Pr // inject policy conditions await this.utils.injectAuthGuard(args, this.model, 'read'); + + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`aggregate\`:\n${formatObject(args)}`); + } return this.modelClient.aggregate(args); } @@ -295,6 +321,9 @@ export class PolicyProxyHandler implements Pr // inject policy conditions await this.utils.injectAuthGuard(args, this.model, 'read'); + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`groupBy\`:\n${formatObject(args)}`); + } return this.modelClient.groupBy(args); } @@ -304,6 +333,10 @@ export class PolicyProxyHandler implements Pr // inject policy conditions args = args ?? {}; await this.utils.injectAuthGuard(args, this.model, 'read'); + + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`count\`:\n${formatObject(args)}`); + } return this.modelClient.count(args); } @@ -323,7 +356,7 @@ export class PolicyProxyHandler implements Pr const readArgs = { select: origArgs.select, include: origArgs.include, where: ids }; const result = await this.utils.readWithCheck(this.model, readArgs); if (result.length === 0) { - this.logger.warn(`${action} result cannot be read back`); + this.logger.info(`${action} result cannot be read back`); throw this.utils.deniedByPolicy( this.model, operation, diff --git a/packages/runtime/src/enhancements/policy/index.ts b/packages/runtime/src/enhancements/policy/index.ts index e54495cb9..90101f264 100644 --- a/packages/runtime/src/enhancements/policy/index.ts +++ b/packages/runtime/src/enhancements/policy/index.ts @@ -13,6 +13,26 @@ export type WithPolicyContext = { user?: AuthUser; }; +/** + * Options for @see withPolicy + */ +export type WithPolicyOptions = { + /** + * Policy definition + */ + policy?: PolicyDef; + + /** + * Model metatadata + */ + modelMeta?: ModelMeta; + + /** + * Whether to log Prisma query + */ + logPrismaQuery?: boolean; +}; + /** * Gets an enhanced Prisma client with access policy check. * @@ -24,16 +44,22 @@ export type WithPolicyContext = { export function withPolicy( prisma: DbClient, context?: WithPolicyContext, - policy?: PolicyDef, - modelMeta?: ModelMeta + options?: WithPolicyOptions ): DbClient { - const _policy = policy ?? getDefaultPolicy(); - const _modelMeta = modelMeta ?? getDefaultModelMeta(); + const _policy = options?.policy ?? getDefaultPolicy(); + const _modelMeta = options?.modelMeta ?? getDefaultModelMeta(); return makeProxy( prisma, _modelMeta, (_prisma, model) => - new PolicyProxyHandler(_prisma as DbClientContract, _policy, _modelMeta, model, context?.user), + new PolicyProxyHandler( + _prisma as DbClientContract, + _policy, + _modelMeta, + model, + context?.user, + options?.logPrismaQuery + ), 'policy' ); } diff --git a/packages/runtime/src/enhancements/policy/logger.ts b/packages/runtime/src/enhancements/policy/logger.ts index a3e7d7fbf..916f7fda2 100644 --- a/packages/runtime/src/enhancements/policy/logger.ts +++ b/packages/runtime/src/enhancements/policy/logger.ts @@ -6,13 +6,27 @@ import { EventEmitter } from 'stream'; * A logger that uses an existing Prisma client to emit. */ export class Logger { - constructor(private readonly prisma: any) {} + private emitter: EventEmitter | undefined; + private eventNames: Array = []; - private get emitter() { + constructor(private readonly prisma: any) { const engine = (this.prisma as any).getEngine(); - return engine ? (engine.logEmitter as EventEmitter) : undefined; + this.emitter = engine ? (engine.logEmitter as EventEmitter) : undefined; + if (this.emitter) { + this.eventNames = this.emitter.eventNames(); + } } + /** + * Checks if a log level is enabled. + */ + public enabled(level: 'info' | 'warn' | 'error') { + return !!this.eventNames.includes(level); + } + + /** + * Generates a message with the given level. + */ public log(level: 'info' | 'warn' | 'error', message: string) { this.emitter?.emit(level, { timestamp: new Date(), diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index c53f94572..a05662969 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -20,6 +20,7 @@ import { NestedWriteVisitor, VisitorContext } from '../nested-write-vistor'; import { ModelMeta, PolicyDef, PolicyFunc } from '../types'; import { enumerate, + formatObject, getIdFields, getModelFields, prismaClientKnownRequestError, @@ -39,7 +40,8 @@ export class PolicyUtil { private readonly db: DbClientContract, private readonly modelMeta: ModelMeta, private readonly policy: PolicyDef, - private readonly user?: AuthUser + private readonly user?: AuthUser, + private readonly logPrismaQuery?: boolean ) { this.logger = new Logger(db); } @@ -247,9 +249,9 @@ export class PolicyUtil { // recursively inject read guard conditions into the query args await this.injectNestedReadConditions(model, args); - // DEBUG - // this.logger.info(`Reading with validation for ${model}: ${formatObject(args)}`); - + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info(`[withPolicy] \`findMany\`:\n${formatObject(args)}`); + } const result: any[] = await this.db[model].findMany(args); await this.postProcessForRead(result, model, args, 'read'); @@ -358,8 +360,11 @@ export class PolicyUtil { const ids = this.getEntityIds(fieldInfo.type, fieldData); if (Object.keys(ids).length !== 0) { - // DEBUG - // this.logger.info(`Validating read of to-one relation: ${fieldInfo.type}#${formatObject(ids)}`); + if (this.logger.enabled('info')) { + this.logger.info( + `Validating read of to-one relation: ${fieldInfo.type}#${formatObject(ids)}` + ); + } await this.checkPolicyForFilter(fieldInfo.type, ids, operation, this.db); } } @@ -548,9 +553,11 @@ export class PolicyUtil { } const query = { where: filter, select }; - // DEBUG - // this.logger.info(`fetching pre-update entities for ${model}: ${formatObject(query)})}`); - + if (this.logPrismaQuery && this.logger.enabled('info')) { + this.logger.info( + `[withPolicy] \`findMany\` for fetching pre-update entities:\n${formatObject(args)}` + ); + } const entities = await this.db[model].findMany(query); entities.forEach((entity) => { addUpdatedEntity(model, this.getEntityIds(model, entity), entity); @@ -742,8 +749,9 @@ export class PolicyUtil { return; } - // DEBUG - // this.logger.info(`Checking policy for ${model}#${JSON.stringify(filter)} for ${operation}`); + if (this.logger.enabled('info')) { + this.logger.info(`Checking policy for ${model}#${JSON.stringify(filter)} for ${operation}`); + } const queryFilter = deepcopy(filter); @@ -752,7 +760,13 @@ export class PolicyUtil { // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' } await this.flattenGeneratedUniqueField(model, queryFilter); - const count = (await db[model].count({ where: queryFilter })) as number; + const countArgs = { where: queryFilter }; + // if (this.logPrismaQuery && this.logger.enabled('info')) { + // this.logger.info( + // `[withPolicy] \`count\` for policy check without guard:\n${formatObject(countArgs)}` + // ); + // } + const count = (await db[model].count(countArgs)) as number; if (count === 0) { // there's nothing to filter out return; @@ -768,10 +782,16 @@ export class PolicyUtil { if (schema) { // we've got schemas, so have to fetch entities and validate them + // if (this.logPrismaQuery && this.logger.enabled('info')) { + // this.logger.info( + // `[withPolicy] \`findMany\` for policy check with guard:\n${formatObject(countArgs)}` + // ); + // } const entities = await db[model].findMany(guardedQuery); if (entities.length < count) { - // DEBUG - // this.logger.info(`entity ${model} failed policy check for operation ${operation}`); + if (this.logger.enabled('info')) { + this.logger.info(`entity ${model} failed policy check for operation ${operation}`); + } throw this.deniedByPolicy( model, operation, @@ -783,16 +803,23 @@ export class PolicyUtil { const schemaCheckErrors = entities.map((entity) => schema.safeParse(entity)).filter((r) => !r.success); if (schemaCheckErrors.length > 0) { const error = schemaCheckErrors.map((r) => !r.success && fromZodError(r.error).message).join(', '); - // DEBUG - // this.logger.info(`entity ${model} failed schema check for operation ${operation}: ${error}`); + if (this.logger.enabled('info')) { + this.logger.info(`entity ${model} failed schema check for operation ${operation}: ${error}`); + } throw this.deniedByPolicy(model, operation, `entities failed schema check: [${error}]`); } } else { // count entities with policy injected and see if any of them are filtered out + // if (this.logPrismaQuery && this.logger.enabled('info')) { + // this.logger.info( + // `[withPolicy] \`count\` for policy check with guard:\n${formatObject(guardedQuery)}` + // ); + // } const guardedCount = (await db[model].count(guardedQuery)) as number; if (guardedCount < count) { - // DEBUG - // this.logger.info(`entity ${model} failed policy check for operation ${operation}`); + if (this.logger.enabled('info')) { + this.logger.info(`entity ${model} failed policy check for operation ${operation}`); + } throw this.deniedByPolicy( model, operation, @@ -808,8 +835,9 @@ export class PolicyUtil { db: Record, preValue: any ) { - // DEBUG - // this.logger.info(`Checking post-update policy for ${model}#${ids}, preValue: ${formatObject(preValue)}`); + if (this.logger.enabled('info')) { + this.logger.info(`Checking post-update policy for ${model}#${ids}, preValue: ${formatObject(preValue)}`); + } const guard = await this.getAuthGuard(model, 'postUpdate', preValue); @@ -821,8 +849,9 @@ export class PolicyUtil { // see if we get fewer items with policy, if so, reject with an throw if (!entity) { - // DEBUG - // this.logger.info(`entity ${model} failed policy check for operation postUpdate`); + if (this.logger.enabled('info')) { + this.logger.info(`entity ${model} failed policy check for operation postUpdate`); + } throw this.deniedByPolicy(model, 'postUpdate'); } @@ -832,8 +861,9 @@ export class PolicyUtil { const schemaCheckResult = schema.safeParse(entity); if (!schemaCheckResult.success) { const error = fromZodError(schemaCheckResult.error).message; - // DEBUG - // this.logger.info(`entity ${model} failed schema check for operation postUpdate: ${error}`); + if (this.logger.enabled('info')) { + this.logger.info(`entity ${model} failed schema check for operation postUpdate: ${error}`); + } throw this.deniedByPolicy(model, 'postUpdate', `entity failed schema check: ${error}`); } } diff --git a/packages/runtime/src/enhancements/preset.ts b/packages/runtime/src/enhancements/preset.ts index e28bdbb2e..1eff8ad28 100644 --- a/packages/runtime/src/enhancements/preset.ts +++ b/packages/runtime/src/enhancements/preset.ts @@ -1,7 +1,11 @@ -import { withOmit } from './omit'; -import { withPassword } from './password'; -import { withPolicy, WithPolicyContext } from './policy'; -import { ModelMeta, PolicyDef } from './types'; +import { withOmit, WithOmitOptions } from './omit'; +import { withPassword, WithPasswordOptions } from './password'; +import { withPolicy, WithPolicyContext, WithPolicyOptions } from './policy'; + +/** + * Options @see withPresets + */ +export type WithPresetsOptions = WithPolicyOptions & WithPasswordOptions & WithOmitOptions; /** * Gets a Prisma client enhanced with all essential behaviors, including access @@ -19,8 +23,7 @@ import { ModelMeta, PolicyDef } from './types'; export function withPresets( prisma: DbClient, context?: WithPolicyContext, - policy?: PolicyDef, - modelMeta?: ModelMeta + options?: WithPresetsOptions ) { - return withPolicy(withOmit(withPassword(prisma, modelMeta), modelMeta), context, policy, modelMeta); + return withPolicy(withOmit(withPassword(prisma, options), options), context, options); } diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts index 69ff16670..0b7f5921e 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -49,7 +49,7 @@ export function enumerate(x: Enumerable) { * Formats an object for pretty printing. */ export function formatObject(value: unknown) { - return util.formatWithOptions({ depth: 10 }, value); + return util.formatWithOptions({ depth: 20 }, value); } let _PrismaClientValidationError: new (...args: unknown[]) => Error; diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 59acc8bd5..4a8598bf9 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -1849,7 +1849,7 @@ describe('REST server tests - enhanced prisma', () => { beforeAll(async () => { const params = await loadSchema(schema); - prisma = withPolicy(params.prisma, undefined, params.policy, params.modelMeta); + prisma = withPolicy(params.prisma, undefined, params); zodSchemas = params.zodSchemas; modelMeta = params.modelMeta; @@ -1950,7 +1950,7 @@ describe('REST server tests - NextAuth project regression', () => { beforeAll(async () => { const params = await loadSchema(schema); - prisma = withPolicy(params.prisma, undefined, params.policy, params.modelMeta); + prisma = withPolicy(params.prisma, undefined, params); zodSchemas = params.zodSchemas; modelMeta = params.modelMeta; diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 0431cac1a..05e1c9f6d 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -94,9 +94,9 @@ plugin zod { } `; -export async function loadSchemaFromFile(schemaFile: string, addPrelude = true, pushDb = true) { +export async function loadSchemaFromFile(schemaFile: string, addPrelude = true, pushDb = true, logPrismaQuery = false) { const content = fs.readFileSync(schemaFile, { encoding: 'utf-8' }); - return loadSchema(content, addPrelude, pushDb); + return loadSchema(content, addPrelude, pushDb, [], false, undefined, logPrismaQuery); } export async function loadSchema( @@ -105,7 +105,8 @@ export async function loadSchema( pushDb = true, extraDependencies: string[] = [], compile = false, - customSchemaFilePath?: string + customSchemaFilePath?: string, + logPrismaQuery = false ) { const { name: projectRoot } = tmp.dirSync({ unsafeCleanup: true }); @@ -195,10 +196,12 @@ export async function loadSchema( return { projectDir: projectRoot, prisma, - withPolicy: (user?: AuthUser) => withPolicy(prisma, { user }, policy, modelMeta), - withOmit: () => withOmit(prisma, modelMeta), - withPassword: () => withPassword(prisma, modelMeta), - withPresets: (user?: AuthUser) => withPresets(prisma, { user }, policy, modelMeta), + withPolicy: (user?: AuthUser) => + withPolicy(prisma, { user }, { policy, modelMeta, logPrismaQuery }), + withOmit: () => withOmit(prisma, { modelMeta }), + withPassword: () => withPassword(prisma, { modelMeta }), + withPresets: (user?: AuthUser) => + withPresets(prisma, { user }, { policy, modelMeta, logPrismaQuery }), policy, modelMeta, zodSchemas,