mirror of
https://github.com/zenstackhq/zenstack
synced 2026-05-24 10:08:55 +00:00
fix(policy): allow auth(). calls in filter functions (#1771)
This commit is contained in:
parent
374e9627bf
commit
aae9b60bf0
6 changed files with 154 additions and 14 deletions
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(`
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
`
|
||||
|
|
|
|||
98
tests/regression/tests/issue-1745.test.ts
Normal file
98
tests/regression/tests/issue-1745.test.ts
Normal 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue