Skip to content

Commit 62a8200

Browse files
authored
fix: when field policy only has deny rule, access should be allowed when the rule doesn't satisfy (#818)
1 parent 83869f2 commit 62a8200

File tree

3 files changed

+107
-4
lines changed

3 files changed

+107
-4
lines changed

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ export class PolicyUtil {
143143

144144
const result: any = {};
145145
for (const [key, value] of Object.entries<any>(condition)) {
146+
if (this.isFalse(result)) {
147+
// already false, no need to continue
148+
break;
149+
}
150+
146151
if (value === null || value === undefined) {
147152
result[key] = value;
148153
continue;
@@ -184,7 +189,16 @@ export class PolicyUtil {
184189
}
185190

186191
case 'NOT': {
187-
result[key] = this.reduce(value);
192+
const r = this.reduce(value);
193+
if (this.isFalse(r)) {
194+
// NOT false => true, thus eliminated (not adding into result)
195+
} else if (this.isTrue(r)) {
196+
// NOT true => false, eliminate all other keys and set entire condition to false
197+
Object.keys(result).forEach((k) => delete result[k]);
198+
result['OR'] = []; // this will cause the outer loop to exit too
199+
} else {
200+
result[key] = r;
201+
}
188202
break;
189203
}
190204

@@ -256,6 +270,7 @@ export class PolicyUtil {
256270
}
257271

258272
if (!provider) {
273+
// field access is allowed by default
259274
return this.makeTrue();
260275
}
261276
const r = provider({ user: this.user }, db);

packages/schema/src/plugins/access-policy/policy-guard-generator.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,19 @@ export default class PolicyGenerator {
606606
throw err;
607607
}
608608
}
609-
writer.write(`return ${FALSE};`);
609+
610+
if (forField) {
611+
if (allows.length === 0) {
612+
// if there's no allow rule, for field-level rules, by default we allow
613+
writer.write(`return ${TRUE};`);
614+
} else {
615+
// if there's any allow rule, we deny unless any allow rule evaluates to true
616+
writer.write(`return ${FALSE};`);
617+
}
618+
} else {
619+
// for model-level rules, the default is always deny
620+
writer.write(`return ${FALSE};`);
621+
}
610622
});
611623
} else {
612624
statements.push((writer) => {
@@ -634,14 +646,17 @@ export default class PolicyGenerator {
634646
};
635647

636648
if (allows.length > 0 && denies.length > 0) {
649+
// include both allow and deny rules
637650
writer.write('{ AND: [');
638651
writeDenies();
639652
writer.write(',');
640653
writeAllows();
641654
writer.write(']}');
642655
} else if (denies.length > 0) {
656+
// only deny rules
643657
writeDenies();
644658
} else if (allows.length > 0) {
659+
// only allow rules
645660
writeAllows();
646661
} else {
647662
// disallow any operation

tests/integration/tests/enhancements/with-policy/field-level-policy.test.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,7 @@ describe('With Policy: field-level policy', () => {
132132
133133
@@allow('all', true)
134134
}
135-
`,
136-
{ logPrismaQuery: true }
135+
`
137136
);
138137

139138
await prisma.user.create({ data: { id: 1, admin: true } });
@@ -841,4 +840,78 @@ describe('With Policy: field-level policy', () => {
841840
expect(r.a).toBe(1);
842841
expect(r.b).toBe(2);
843842
});
843+
844+
it('deny only without field access', async () => {
845+
const { prisma, withPolicy } = await loadSchema(
846+
`
847+
model User {
848+
id Int @id @default(autoincrement())
849+
role String @deny('update', auth().role != 'ADMIN')
850+
851+
@@allow('all', true)
852+
}
853+
`
854+
);
855+
856+
const user = await prisma.user.create({
857+
data: { role: 'USER' },
858+
});
859+
860+
await expect(
861+
withPolicy({ id: 1, role: 'ADMIN' }).user.update({
862+
where: { id: user.id },
863+
data: { role: 'ADMIN' },
864+
})
865+
).toResolveTruthy();
866+
867+
await expect(
868+
withPolicy({ id: 1, role: 'USER' }).user.update({
869+
where: { id: user.id },
870+
data: { role: 'ADMIN' },
871+
})
872+
).toBeRejectedByPolicy();
873+
});
874+
875+
it('deny only with field access', async () => {
876+
const { prisma, withPolicy } = await loadSchema(
877+
`
878+
model User {
879+
id Int @id @default(autoincrement())
880+
locked Boolean @default(false)
881+
role String @deny('update', auth().role != 'ADMIN' || locked)
882+
883+
@@allow('all', true)
884+
}
885+
`
886+
);
887+
888+
const user1 = await prisma.user.create({
889+
data: { role: 'USER' },
890+
});
891+
892+
await expect(
893+
withPolicy({ id: 1, role: 'ADMIN' }).user.update({
894+
where: { id: user1.id },
895+
data: { role: 'ADMIN' },
896+
})
897+
).toResolveTruthy();
898+
899+
await expect(
900+
withPolicy({ id: 1, role: 'USER' }).user.update({
901+
where: { id: user1.id },
902+
data: { role: 'ADMIN' },
903+
})
904+
).toBeRejectedByPolicy();
905+
906+
const user2 = await prisma.user.create({
907+
data: { role: 'USER', locked: true },
908+
});
909+
910+
await expect(
911+
withPolicy({ id: 1, role: 'ADMIN' }).user.update({
912+
where: { id: user2.id },
913+
data: { role: 'ADMIN' },
914+
})
915+
).toBeRejectedByPolicy();
916+
});
844917
});

0 commit comments

Comments
 (0)