Skip to content

Commit d8c2e87

Browse files
authored
feat: implementing access control for Prisma Pulse (#643)
1 parent 38e89e4 commit d8c2e87

File tree

6 files changed

+344
-7
lines changed

6 files changed

+344
-7
lines changed

packages/runtime/src/enhancements/policy/handler.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,8 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
524524
throw prismaClientValidationError(this.prisma, 'data field is required in query argument');
525525
}
526526

527+
args = this.utils.clone(args);
528+
527529
const { result, error } = await this.transaction(async (tx) => {
528530
// proceed with nested writes and collect post-write checks
529531
const { result, postWriteChecks } = await this.doUpdate(args, tx);
@@ -543,8 +545,6 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
543545
}
544546

545547
private async doUpdate(args: any, db: Record<string, DbOperations>) {
546-
args = this.utils.clone(args);
547-
548548
// collected post-update checks
549549
const postWriteChecks: PostWriteCheckRecord[] = [];
550550

@@ -903,6 +903,8 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
903903
await this.utils.tryReject(this.prisma, this.model, 'create');
904904
await this.utils.tryReject(this.prisma, this.model, 'update');
905905

906+
args = this.utils.clone(args);
907+
906908
// We can call the native "upsert" because we can't tell if an entity was created or updated
907909
// for doing post-write check accordingly. Instead, decompose it into create or update.
908910

@@ -998,6 +1000,8 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
9981000
throw prismaClientValidationError(this.prisma, 'query argument is required');
9991001
}
10001002

1003+
args = this.utils.clone(args);
1004+
10011005
// inject policy conditions
10021006
await this.utils.injectAuthGuard(this.prisma, args, this.model, 'read');
10031007

@@ -1012,6 +1016,8 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
10121016
throw prismaClientValidationError(this.prisma, 'query argument is required');
10131017
}
10141018

1019+
args = this.utils.clone(args);
1020+
10151021
// inject policy conditions
10161022
await this.utils.injectAuthGuard(this.prisma, args, this.model, 'read');
10171023

@@ -1023,7 +1029,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
10231029

10241030
async count(args: any) {
10251031
// inject policy conditions
1026-
args = args ?? {};
1032+
args = args ? this.utils.clone(args) : {};
10271033
await this.utils.injectAuthGuard(this.prisma, args, this.model, 'read');
10281034

10291035
if (this.shouldLogQuery) {
@@ -1034,6 +1040,55 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
10341040

10351041
//#endregion
10361042

1043+
//#region Subscribe (Prisma Pulse)
1044+
1045+
async subscribe(args: any) {
1046+
const readGuard = this.utils.getAuthGuard(this.prisma, this.model, 'read');
1047+
if (this.utils.isTrue(readGuard)) {
1048+
// no need to inject
1049+
if (this.shouldLogQuery) {
1050+
this.logger.info(`[policy] \`subscribe\` ${this.model}:\n${formatObject(args)}`);
1051+
}
1052+
return this.modelClient.subscribe(args);
1053+
}
1054+
1055+
if (!args) {
1056+
// include all
1057+
args = { create: {}, update: {}, delete: {} };
1058+
} else {
1059+
if (typeof args !== 'object') {
1060+
throw prismaClientValidationError(this.prisma, 'argument must be an object');
1061+
}
1062+
if (Object.keys(args).length === 0) {
1063+
// include all
1064+
args = { create: {}, update: {}, delete: {} };
1065+
} else {
1066+
args = this.utils.clone(args);
1067+
}
1068+
}
1069+
1070+
// inject into subscribe conditions
1071+
1072+
if (args.create) {
1073+
args.create.after = this.utils.and(args.create.after, readGuard);
1074+
}
1075+
1076+
if (args.update) {
1077+
args.update.after = this.utils.and(args.update.after, readGuard);
1078+
}
1079+
1080+
if (args.delete) {
1081+
args.delete.before = this.utils.and(args.delete.before, readGuard);
1082+
}
1083+
1084+
if (this.shouldLogQuery) {
1085+
this.logger.info(`[policy] \`subscribe\` ${this.model}:\n${formatObject(args)}`);
1086+
}
1087+
return this.modelClient.subscribe(args);
1088+
}
1089+
1090+
//#endregion
1091+
10371092
//#region Utils
10381093

10391094
private get shouldLogQuery() {

packages/runtime/src/enhancements/policy/policy-utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export class PolicyUtil {
8181
// Static True/False conditions
8282
// https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined#the-effect-of-null-and-undefined-on-conditionals
8383

84-
private isTrue(condition: object) {
84+
public isTrue(condition: object) {
8585
if (condition === null || condition === undefined) {
8686
return false;
8787
} else {
@@ -92,7 +92,7 @@ export class PolicyUtil {
9292
}
9393
}
9494

95-
private isFalse(condition: object) {
95+
public isFalse(condition: object) {
9696
if (condition === null || condition === undefined) {
9797
return false;
9898
} else {

packages/runtime/src/enhancements/proxy.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export interface PrismaProxyHandler {
4242
groupBy(args: any): Promise<unknown>;
4343

4444
count(args: any): Promise<unknown | number>;
45+
46+
subscribe(args: any): Promise<unknown>;
4547
}
4648

4749
/**
@@ -141,6 +143,11 @@ export class DefaultPrismaProxyHandler implements PrismaProxyHandler {
141143
return this.prisma[this.model].count(args);
142144
}
143145

146+
async subscribe(args: any): Promise<unknown> {
147+
args = await this.preprocessArgs('subscribe', args);
148+
return this.prisma[this.model].subscribe(args);
149+
}
150+
144151
/**
145152
* Processes result entities before they're returned
146153
*/

packages/runtime/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface DbOperations {
1919
aggregate(args: unknown): Promise<unknown>;
2020
groupBy(args: unknown): Promise<unknown>;
2121
count(args?: unknown): Promise<unknown>;
22+
subscribe(args?: unknown): Promise<unknown>;
2223
fields: Record<string, any>;
2324
}
2425

packages/testtools/src/schema.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export type SchemaLoadOptions = {
9999
logPrismaQuery?: boolean;
100100
provider?: 'sqlite' | 'postgresql';
101101
dbUrl?: string;
102+
pulseApiKey?: string;
102103
};
103104

104105
const defaultOptions: SchemaLoadOptions = {
@@ -187,14 +188,23 @@ export async function loadSchema(schema: string, options?: SchemaLoadOptions) {
187188
run('npx prisma db push');
188189
}
189190

190-
const PrismaClient = require(path.join(projectRoot, 'node_modules/.prisma/client')).PrismaClient;
191-
const prisma = new PrismaClient({ log: ['info', 'warn', 'error'] });
191+
if (opt.pulseApiKey) {
192+
opt.extraDependencies?.push('@prisma/extension-pulse');
193+
}
192194

193195
opt.extraDependencies?.forEach((dep) => {
194196
console.log(`Installing dependency ${dep}`);
195197
run(`npm install ${dep}`);
196198
});
197199

200+
const PrismaClient = require(path.join(projectRoot, 'node_modules/.prisma/client')).PrismaClient;
201+
let prisma = new PrismaClient({ log: ['info', 'warn', 'error'] });
202+
203+
if (opt.pulseApiKey) {
204+
const withPulse = require(path.join(projectRoot, 'node_modules/@prisma/extension-pulse/dist/cjs')).withPulse;
205+
prisma = prisma.$extends(withPulse({ apiKey: opt.pulseApiKey }));
206+
}
207+
198208
if (opt.compile) {
199209
console.log('Compiling...');
200210
run('npx tsc --init');

0 commit comments

Comments
 (0)