diff --git a/packages/runtime/src/enhancements/node/policy/handler.ts b/packages/runtime/src/enhancements/node/policy/handler.ts index 91ab24c07..d88baa222 100644 --- a/packages/runtime/src/enhancements/node/policy/handler.ts +++ b/packages/runtime/src/enhancements/node/policy/handler.ts @@ -116,7 +116,7 @@ export class PolicyProxyHandler implements Pr } findMany(args?: any) { - return createDeferredPromise(() => this.doFind(args, 'findMany', () => [])); + return createDeferredPromise(() => this.doFind(args, 'findMany', () => [], true)); } // make a find query promise with fluent API call stubs installed @@ -130,10 +130,10 @@ export class PolicyProxyHandler implements Pr ); } - private async doFind(args: any, actionName: FindOperations, handleRejection: () => any) { + private async doFind(args: any, actionName: FindOperations, handleRejection: () => any, isList: boolean = false) { const origArgs = args; const _args = this.policyUtils.safeClone(args); - if (!this.policyUtils.injectForRead(this.prisma, this.model, _args)) { + if (!this.policyUtils.injectForReadOrList(this.prisma, this.model, _args, isList)) { if (this.shouldLogQuery) { this.logger.info(`[policy] \`${actionName}\` ${this.model}: unconditionally denied`); } @@ -1609,7 +1609,7 @@ export class PolicyProxyHandler implements Pr // "update" has an extra layer of "after" const payload = key === 'update' ? args[key].after : args[key]; const toInject = { where: payload }; - this.policyUtils.injectForRead(this.prisma, this.model, toInject); + this.policyUtils.injectForReadOrList(this.prisma, this.model, toInject, false); if (key === 'update') { // "update" has an extra layer of "after" args[key].after = toInject.where; diff --git a/packages/runtime/src/enhancements/node/policy/index.ts b/packages/runtime/src/enhancements/node/policy/index.ts index d5523e31b..a68385237 100644 --- a/packages/runtime/src/enhancements/node/policy/index.ts +++ b/packages/runtime/src/enhancements/node/policy/index.ts @@ -75,6 +75,6 @@ export async function policyProcessIncludeRelationPayload( context: EnhancementContext | undefined ) { const utils = new PolicyUtil(prisma, options, context); - await utils.injectForRead(prisma, model, payload); + await utils.injectForReadOrList(prisma, model, payload, false); await utils.injectReadCheckSelect(model, payload); } diff --git a/packages/runtime/src/enhancements/node/policy/policy-utils.ts b/packages/runtime/src/enhancements/node/policy/policy-utils.ts index b39ac5b00..f9d26aa99 100644 --- a/packages/runtime/src/enhancements/node/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/node/policy/policy-utils.ts @@ -277,6 +277,7 @@ export class PolicyUtil extends QueryUtils { create: { guard: true, inputChecker: true }, update: { guard: true }, delete: { guard: true }, + list: { guard: true }, postUpdate: { guard: true }, }, }; @@ -603,7 +604,7 @@ export class PolicyUtil extends QueryUtils { /** * Injects auth guard for read operations. */ - injectForRead(db: CrudContract, model: string, args: any) { + injectForReadOrList(db: CrudContract, model: string, args: any, isList: boolean) { // make select and include visible to the injection const injected: any = { select: args.select, include: args.include }; if (!this.injectAuthGuardAsWhere(db, injected, model, 'read')) { @@ -611,6 +612,11 @@ export class PolicyUtil extends QueryUtils { return false; } + if (!this.injectAuthGuardAsWhere(db, injected, model, 'list')) { + args.where = this.makeFalse(); + return false; + } + if (args.where) { // inject into fields: // to-many: some/none/every @@ -1134,7 +1140,7 @@ export class PolicyUtil extends QueryUtils { CrudFailureReason.RESULT_NOT_READABLE ); - const injectResult = this.injectForRead(db, model, readArgs); + const injectResult = this.injectForReadOrList(db, model, readArgs, false); if (!injectResult) { return { error, result: undefined }; } diff --git a/packages/runtime/src/enhancements/node/types.ts b/packages/runtime/src/enhancements/node/types.ts index c9a90baa8..31caba26d 100644 --- a/packages/runtime/src/enhancements/node/types.ts +++ b/packages/runtime/src/enhancements/node/types.ts @@ -140,6 +140,7 @@ export type ModelCrudDef = { create: ModelCreateDef; update: ModelUpdateDef; delete: ModelDeleteDef; + list: ModelListDef; postUpdate: ModelPostUpdateDef; }; @@ -207,6 +208,11 @@ type ModelUpdateDef = ModelCrudCommon; */ type ModelDeleteDef = ModelCrudCommon; +/** + * Policy definition for listing a model + */ +type ModelListDef = ModelCrudCommon; + /** * Policy definition for post-update checking a model */ diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index fe31a5058..7391d2aa1 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -36,7 +36,7 @@ export interface DbOperations { */ export type PolicyKind = 'allow' | 'deny'; -export type PolicyCrudKind = 'read' | 'create' | 'update' | 'delete'; +export type PolicyCrudKind = 'read' | 'create' | 'update' | 'delete' | 'list'; /** * Kinds of operations controlled by access policies diff --git a/packages/schema/src/language-server/validator/attribute-application-validator.ts b/packages/schema/src/language-server/validator/attribute-application-validator.ts index 0e1d8e885..a06816739 100644 --- a/packages/schema/src/language-server/validator/attribute-application-validator.ts +++ b/packages/schema/src/language-server/validator/attribute-application-validator.ts @@ -137,7 +137,7 @@ export default class AttributeApplicationValidator implements AstValidator { this.writeCommonModelDef(model, 'delete', policies, writer, sourceFile); }); + writer.writeLine(','); + } + + // writes `list: ...` for a given model + private writeModelListDef( + model: DataModel, + policies: PolicyAnalysisResult, + writer: CodeBlockWriter, + sourceFile: SourceFile + ) { + writer.write(`list:`); + writer.inlineBlock(() => { + this.writeCommonModelDef(model, 'list', policies, writer, sourceFile); + }); + writer.writeLine(','); } // writes `[kind]: ...` for a given model diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index a0a0a41f8..824661c6a 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -527,7 +527,7 @@ attribute @@schema(_ name: String) @@@prisma * @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations. * @param condition: a boolean expression that controls if the operation should be allowed. */ -attribute @@allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean) +attribute @@allow(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'list'", "'all'"]), _ condition: Boolean) /** * Defines an access policy that allows the annotated field to be read or updated. @@ -545,7 +545,7 @@ attribute @allow(_ operation: String @@@completionHint(["'create'", "'read'", "' * @param operation: comma-separated list of "create", "read", "update", "delete". Use "all" to denote all operations. * @param condition: a boolean expression that controls if the operation should be denied. */ -attribute @@deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'all'"]), _ condition: Boolean) +attribute @@deny(_ operation: String @@@completionHint(["'create'", "'read'", "'update'", "'delete'", "'list'", "'all'"]), _ condition: Boolean) /** * Defines an access policy that denies the annotated field to be read or updated. diff --git a/packages/sdk/src/policy.ts b/packages/sdk/src/policy.ts index c9eea9865..d3da674fd 100644 --- a/packages/sdk/src/policy.ts +++ b/packages/sdk/src/policy.ts @@ -12,6 +12,7 @@ export function analyzePolicies(dataModel: DataModel) { const read = toStaticPolicy('read', allows, denies); const update = toStaticPolicy('update', allows, denies); const del = toStaticPolicy('delete', allows, denies); + const list = toStaticPolicy('list', allows, denies); const hasFieldValidation = hasValidationAttributes(dataModel); return { @@ -21,6 +22,7 @@ export function analyzePolicies(dataModel: DataModel) { read, update, delete: del, + list, allowAll: create === true && read === true && update === true && del === true, denyAll: create === false && read === false && update === false && del === false, hasFieldValidation, diff --git a/packages/sdk/src/typescript-expression-transformer.ts b/packages/sdk/src/typescript-expression-transformer.ts index 801db4d4f..9d20076c5 100644 --- a/packages/sdk/src/typescript-expression-transformer.ts +++ b/packages/sdk/src/typescript-expression-transformer.ts @@ -38,7 +38,7 @@ type Options = { thisExprContext?: string; futureRefContext?: string; context: ExpressionContext; - operationContext?: 'read' | 'create' | 'update' | 'postUpdate' | 'delete'; + operationContext?: 'read' | 'create' | 'update' | 'postUpdate' | 'delete' | 'list'; }; type Casing = 'original' | 'upper' | 'lower' | 'capitalize' | 'uncapitalize'; diff --git a/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts b/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts index 3543dd7b5..f7f3c63dc 100644 --- a/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts +++ b/tests/integration/tests/enhancements/with-policy/toplevel-operations.test.ts @@ -271,4 +271,38 @@ describe('With Policy: toplevel operations', () => { expect(await db.model.deleteMany()).toEqual(expect.objectContaining({ count: 0 })); }); + + it('list tests', async () => { + const { enhance, prisma } = await loadSchema( + ` + model Model { + id String @id @default(uuid()) + value Int + + @@allow('create', true) + @@allow('read', true) + @@allow('list', false) + } + ` + ); + + const db = enhance(); + + // Create some items + await db.model.createMany({ + data: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }], + }); + + const fromPrisma = await prisma.model.findMany(); + expect(fromPrisma).toHaveLength(4); + + const fromDb = await db.model.findMany(); + console.log(fromDb); + // const firstItem = await db.model.findFirst(); + + // expect(firstItem).toBeTruthy(); + + // listing denied + // expect(fromDb).toHaveLength(0); + }); });