Skip to content
Draft
8 changes: 4 additions & 4 deletions packages/runtime/src/enhancements/node/policy/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
}

findMany(args?: any) {
return createDeferredPromise<unknown[]>(() => this.doFind(args, 'findMany', () => []));
return createDeferredPromise<unknown[]>(() => this.doFind(args, 'findMany', () => [], true));
}

// make a find query promise with fluent API call stubs installed
Expand All @@ -130,10 +130,10 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> 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`);
}
Expand Down Expand Up @@ -1609,7 +1609,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> 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;
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/enhancements/node/policy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
10 changes: 8 additions & 2 deletions packages/runtime/src/enhancements/node/policy/policy-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
};
Expand Down Expand Up @@ -603,14 +604,19 @@ 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')) {
args.where = this.makeFalse();
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
Expand Down Expand Up @@ -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 };
}
Expand Down
6 changes: 6 additions & 0 deletions packages/runtime/src/enhancements/node/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export type ModelCrudDef = {
create: ModelCreateDef;
update: ModelUpdateDef;
delete: ModelDeleteDef;
list: ModelListDef;
postUpdate: ModelPostUpdateDef;
};

Expand Down Expand Up @@ -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
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
accept('error', `expects a string literal`, { node: attr.args[0] });
return;
}
this.validatePolicyKinds(kind, ['create', 'read', 'update', 'delete', 'all'], attr, accept);
this.validatePolicyKinds(kind, ['create', 'read', 'update', 'delete', 'all', 'list'], attr, accept);

// @encrypted fields cannot be used in policy rules
this.rejectEncryptedFields(attr, accept);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export class PolicyGenerator {
this.writeModelUpdateDef(model, policies, writer, sourceFile);
this.writeModelPostUpdateDef(model, policies, writer, sourceFile);
this.writeModelDeleteDef(model, policies, writer, sourceFile);
this.writeModelListDef(model, policies, writer, sourceFile);
});
writer.writeLine(',');
}
Expand Down Expand Up @@ -347,6 +348,21 @@ export class PolicyGenerator {
writer.inlineBlock(() => {
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
Expand Down
4 changes: 2 additions & 2 deletions packages/schema/src/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/src/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/src/typescript-expression-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Loading