Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>();
Expand Down Expand Up @@ -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,
}
);
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion packages/schema/src/language-server/validator/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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));
}
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,11 @@ describe('Attribute tests', () => {
E2
}

model User {
id String @id
e E
}

model N {
id String @id
e E
Expand All @@ -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))
Expand Down Expand Up @@ -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(`
Expand Down Expand Up @@ -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(`
Expand Down
22 changes: 22 additions & 0 deletions tests/integration/tests/e2e/filter-function-coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
`
Expand Down
98 changes: 98 additions & 0 deletions tests/regression/tests/issue-1745.test.ts
Original file line number Diff line number Diff line change
@@ -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');
}
});
});
Loading