diff --git a/packages/schema/src/language-server/validator/expression-validator.ts b/packages/schema/src/language-server/validator/expression-validator.ts index e6eb18c29..d5a11c7b7 100644 --- a/packages/schema/src/language-server/validator/expression-validator.ts +++ b/packages/schema/src/language-server/validator/expression-validator.ts @@ -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 { // 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)); - } } diff --git a/packages/schema/src/language-server/validator/function-invocation-validator.ts b/packages/schema/src/language-server/validator/function-invocation-validator.ts index 79b2b1a6b..f37886c93 100644 --- a/packages/schema/src/language-server/validator/function-invocation-validator.ts +++ b/packages/schema/src/language-server/validator/function-invocation-validator.ts @@ -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(); @@ -109,15 +109,24 @@ export default class FunctionInvocationValidator implements AstValidator 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, + } + ); } } } diff --git a/packages/schema/src/language-server/validator/utils.ts b/packages/schema/src/language-server/validator/utils.ts index 6a1a44336..3dd5b537b 100644 --- a/packages/schema/src/language-server/validator/utils.ts +++ b/packages/schema/src/language-server/validator/utils.ts @@ -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)); +} diff --git a/packages/schema/tests/schema/validation/attribute-validation.test.ts b/packages/schema/tests/schema/validation/attribute-validation.test.ts index 778f37d7a..0c89c61ae 100644 --- a/packages/schema/tests/schema/validation/attribute-validation.test.ts +++ b/packages/schema/tests/schema/validation/attribute-validation.test.ts @@ -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(` diff --git a/tests/integration/tests/e2e/filter-function-coverage.test.ts b/tests/integration/tests/e2e/filter-function-coverage.test.ts index 13564095c..04724578b 100644 --- a/tests/integration/tests/e2e/filter-function-coverage.test.ts +++ b/tests/integration/tests/e2e/filter-function-coverage.test.ts @@ -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( ` diff --git a/tests/regression/tests/issue-1745.test.ts b/tests/regression/tests/issue-1745.test.ts new file mode 100644 index 000000000..ece70d6ee --- /dev/null +++ b/tests/regression/tests/issue-1745.test.ts @@ -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'); + } + }); +});