fix(policy): allow auth(). calls in filter functions (#1771)

This commit is contained in:
Yiming 2024-10-10 14:20:24 -07:00 committed by GitHub
parent 374e9627bf
commit aae9b60bf0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 154 additions and 14 deletions

View file

@ -24,7 +24,7 @@ import {
import { ValidationAcceptor, streamAst } from 'langium';
import { findUpAst, getContainingDataModel } from '../../utils/ast-utils';
import { AstValidator } from '../types';
import { typeAssignable } from './utils';
import { isAuthOrAuthMemberAccess, typeAssignable } from './utils';
/**
* Validates expressions.
@ -296,13 +296,9 @@ export default class ExpressionValidator implements AstValidator<Expression> {
// null
isNullExpr(expr) ||
// `auth()` access
this.isAuthOrAuthMemberAccess(expr) ||
isAuthOrAuthMemberAccess(expr) ||
// array
(isArrayExpr(expr) && expr.items.every((item) => this.isNotModelFieldExpr(item)))
);
}
private isAuthOrAuthMemberAccess(expr: Expression) {
return isAuthInvocation(expr) || (isMemberAccessExpr(expr) && isAuthInvocation(expr.operand));
}
}

View file

@ -26,7 +26,7 @@ import { AstNode, streamAst, ValidationAcceptor } from 'langium';
import { match, P } from 'ts-pattern';
import { isCheckInvocation } from '../../utils/ast-utils';
import { AstValidator } from '../types';
import { typeAssignable } from './utils';
import { isAuthOrAuthMemberAccess, typeAssignable } from './utils';
// a registry of function handlers marked with @func
const invocationCheckers = new Map<string, PropertyDescriptor>();
@ -109,15 +109,24 @@ export default class FunctionInvocationValidator implements AstValidator<Express
!isLiteralExpr(secondArg) &&
// enum field
!isEnumFieldReference(secondArg) &&
// `auth()...` expression
!isAuthOrAuthMemberAccess(secondArg) &&
// array of literal/enum
!(
isArrayExpr(secondArg) &&
secondArg.items.every((item) => isLiteralExpr(item) || isEnumFieldReference(item))
secondArg.items.every(
(item) =>
isLiteralExpr(item) || isEnumFieldReference(item) || isAuthOrAuthMemberAccess(item)
)
)
) {
accept('error', 'second argument must be a literal, an enum, or an array of them', {
node: secondArg,
});
accept(
'error',
'second argument must be a literal, an enum, an expression starting with `auth().`, or an array of them',
{
node: secondArg,
}
);
}
}
}

View file

@ -10,10 +10,11 @@ import {
isArrayExpr,
isDataModelField,
isEnum,
isMemberAccessExpr,
isReferenceExpr,
isStringLiteral,
} from '@zenstackhq/language/ast';
import { resolved } from '@zenstackhq/sdk';
import { isAuthInvocation, resolved } from '@zenstackhq/sdk';
import { AstNode, ValidationAcceptor } from 'langium';
/**
@ -181,3 +182,7 @@ export function assignableToAttributeParam(
return (dstRef?.ref === argResolvedType.decl || dstType === 'Any') && dstIsArray === argResolvedType.array;
}
}
export function isAuthOrAuthMemberAccess(expr: Expression): boolean {
return isAuthInvocation(expr) || (isMemberAccessExpr(expr) && isAuthOrAuthMemberAccess(expr.operand));
}

View file

@ -816,6 +816,11 @@ describe('Attribute tests', () => {
E2
}
model User {
id String @id
e E
}
model N {
id String @id
e E
@ -840,6 +845,7 @@ describe('Attribute tests', () => {
@@allow('all', startsWith(s, 'a'))
@@allow('all', endsWith(s, 'a'))
@@allow('all', has(es, E1))
@@allow('all', has(es, auth().e))
@@allow('all', hasSome(es, [E1]))
@@allow('all', hasEvery(es, [E1]))
@@allow('all', isEmpty(es))
@ -890,7 +896,9 @@ describe('Attribute tests', () => {
@@allow('all', contains(s, s1))
}
`)
).toContain('second argument must be a literal, an enum, or an array of them');
).toContain(
'second argument must be a literal, an enum, an expression starting with `auth().`, or an array of them'
);
expect(
await loadModelWithError(`
@ -1022,7 +1030,9 @@ describe('Attribute tests', () => {
@@validate(contains(s, s1))
}
`)
).toContain('second argument must be a literal, an enum, or an array of them');
).toContain(
'second argument must be a literal, an enum, an expression starting with `auth().`, or an array of them'
);
expect(
await loadModelWithError(`

View file

@ -36,6 +36,28 @@ describe('Filter Function Coverage Tests', () => {
await expect(enhance({ id: 'user1', name: 'bac' }).foo.create({ data: {} })).toResolveTruthy();
});
it('contains with auth()', async () => {
const { enhance } = await loadSchema(
`
model User {
id String @id
name String
}
model Foo {
id String @id @default(cuid())
string String
@@allow('all', contains(string, auth().name))
}
`
);
await expect(enhance().foo.create({ data: { string: 'abc' } })).toBeRejectedByPolicy();
const db = enhance({ id: '1', name: 'a' });
await expect(db.foo.create({ data: { string: 'bcd' } })).toBeRejectedByPolicy();
await expect(db.foo.create({ data: { string: 'bac' } })).toResolveTruthy();
});
it('startsWith field', async () => {
const { enhance } = await loadSchema(
`

View file

@ -0,0 +1,98 @@
import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools';
describe('issue 1745', () => {
it('regression', async () => {
const dbUrl = await createPostgresDb('issue-1745');
try {
await loadSchema(
`
enum BuyerType {
STORE
RESTAURANT
WHOLESALER
}
enum ChainStore {
ALL
CHAINSTORE_1
CHAINSTORE_2
CHAINSTORE_3
}
abstract model Id {
id String @id @default(cuid())
}
abstract model Base extends Id {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Ad extends Base {
serial Int @unique @default(autoincrement())
buyerTypes BuyerType[]
chainStores ChainStore[]
listPrice Float
isSold Boolean @default(false)
supplier Supplier @relation(fields: [supplierId], references: [id])
supplierId String @default(auth().companyId)
@@allow('all', auth().company.companyType == 'Buyer' && has(buyerTypes, auth().company.buyerType))
@@allow('all', auth().company.companyType == 'Supplier' && auth().companyId == supplierId)
@@allow('all', auth().isAdmin)
}
model Company extends Base {
name String @unique
organizationNumber String @unique
users User[]
buyerType BuyerType
companyType String
@@delegate(companyType)
@@allow('read, update', auth().companyId == id)
@@allow('all', auth().isAdmin)
}
model Buyer extends Company {
storeName String
type String
chainStore ChainStore @default(ALL)
@@allow('read, update', auth().company.companyType == 'Buyer' && auth().companyId == id)
@@allow('all', auth().isAdmin)
}
model Supplier extends Company {
ads Ad[]
@@allow('all', auth().company.companyType == 'Supplier' && auth().companyId == id)
@@allow('all', auth().isAdmin)
}
model User extends Base {
firstName String
lastName String
email String @unique
username String @unique
password String @password @omit
isAdmin Boolean @default(false)
company Company? @relation(fields: [companyId], references: [id])
companyId String?
@@allow('read', auth().id == id)
@@allow('read', auth().companyId == companyId)
@@allow('all', auth().isAdmin)
}
`,
{ provider: 'postgresql', dbUrl, pushDb: false }
);
} finally {
dropPostgresDb('issue-1745');
}
});
});