mirror of
https://github.com/zenstackhq/zenstack
synced 2026-05-24 10:08:55 +00:00
526 lines
16 KiB
TypeScript
526 lines
16 KiB
TypeScript
import { describe, it } from 'vitest';
|
|
import { loadSchema, loadSchemaWithError } from './utils';
|
|
|
|
describe('Attribute application validation tests', () => {
|
|
describe('Model-level policy attributes (@@allow, @@deny)', () => {
|
|
it('accepts valid policy kinds', async () => {
|
|
await loadSchema(`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
@@allow('create', true)
|
|
@@allow('read', true)
|
|
@@allow('update', true)
|
|
@@allow('post-update', true)
|
|
@@allow('delete', true)
|
|
@@deny('all', false)
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('accepts comma-separated policy kinds', async () => {
|
|
await loadSchema(`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
@@allow('create, read, update', true)
|
|
@@deny('delete, post-update', false)
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('rejects invalid policy kind', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
@@allow('invalid', true)
|
|
}
|
|
`,
|
|
`Invalid policy rule kind`,
|
|
);
|
|
});
|
|
|
|
it('rejects before in create policies', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
@@allow('all', true)
|
|
@@deny('create', before(x) > 2)
|
|
}
|
|
`,
|
|
`"before()" is only allowed in "post-update" policy rules`,
|
|
);
|
|
});
|
|
|
|
it('rejects before in read policies', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
@@allow('all', true)
|
|
@@deny('read', before(x) > 2)
|
|
}
|
|
`,
|
|
`"before()" is only allowed in "post-update" policy rules`,
|
|
);
|
|
});
|
|
|
|
it('rejects before in non-post-update policies', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
@@allow('all', true)
|
|
@@deny('update', before(x) > 2)
|
|
}
|
|
`,
|
|
`"before()" is only allowed in "post-update" policy rules`,
|
|
);
|
|
});
|
|
|
|
it('rejects before in delete policies', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
@@allow('all', true)
|
|
@@deny('delete', before(x) > 2)
|
|
}
|
|
`,
|
|
`"before()" is only allowed in "post-update" policy rules`,
|
|
);
|
|
});
|
|
|
|
it('accepts before in post-update policies', async () => {
|
|
await loadSchema(`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
@@allow('all', true)
|
|
@@deny('post-update', before().x > 2)
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('rejects non-owned relation in create policy', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
bars Bar[]
|
|
@@allow('create', bars?[x > 0])
|
|
}
|
|
|
|
model Bar {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
foo Foo @relation(fields: [fooId], references: [id])
|
|
fooId Int
|
|
@@allow('all', true)
|
|
}
|
|
`,
|
|
`non-owned relation fields are not allowed in "create" rules`,
|
|
);
|
|
});
|
|
|
|
it('rejects non-owned relation in all policy', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
bars Bar[]
|
|
@@allow('all', bars?[x > 0])
|
|
}
|
|
|
|
model Bar {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
foo Foo @relation(fields: [fooId], references: [id])
|
|
fooId Int
|
|
@@allow('all', true)
|
|
}
|
|
`,
|
|
`non-owned relation fields are not allowed in "create" rules`,
|
|
);
|
|
});
|
|
|
|
it('accepts owned relation in create policy', async () => {
|
|
await loadSchema(`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
bar Bar @relation(fields: [barId], references: [id])
|
|
barId Int
|
|
@@allow('create', bar.x > 0)
|
|
}
|
|
|
|
model Bar {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
foos Foo[]
|
|
@@allow('all', true)
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('accepts non-owned relation in read policy', async () => {
|
|
await loadSchema(`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
bars Bar[]
|
|
@@allow('read', bars?[x > 0])
|
|
}
|
|
|
|
model Bar {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
foo Foo @relation(fields: [fooId], references: [id])
|
|
fooId Int
|
|
@@allow('all', true)
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('accepts non-owned relation in update policy', async () => {
|
|
await loadSchema(`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
bars Bar[]
|
|
@@allow('update', bars?[x > 0])
|
|
}
|
|
|
|
model Bar {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
foo Foo @relation(fields: [fooId], references: [id])
|
|
fooId Int
|
|
@@allow('all', true)
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('accepts auth() relation access in create policy', async () => {
|
|
await loadSchema(`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model User {
|
|
id Int @id @default(autoincrement())
|
|
posts Post[]
|
|
@@allow('all', true)
|
|
@@auth()
|
|
}
|
|
|
|
model Post {
|
|
id Int @id @default(autoincrement())
|
|
author User @relation(fields: [authorId], references: [id])
|
|
authorId Int
|
|
@@allow('create', auth().posts?[id > 0])
|
|
}
|
|
`);
|
|
});
|
|
});
|
|
|
|
describe('Field-level policy attributes (@allow, @deny)', () => {
|
|
it('accepts valid field-level policy kinds', async () => {
|
|
await loadSchema(`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
x Int @allow('read', true)
|
|
y Int @allow('update', true)
|
|
z Int @deny('all', false)
|
|
@@allow('all', true)
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('accepts comma-separated field-level policy kinds', async () => {
|
|
await loadSchema(`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
x Int @allow('read, update', true)
|
|
@@allow('all', true)
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('rejects before in field-level policies', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
x Int @deny('update', before(x) > 2)
|
|
@@allow('all', true)
|
|
}
|
|
`,
|
|
`"before()" is not allowed in field-level policies`,
|
|
);
|
|
});
|
|
|
|
it('rejects field-level policy on relation fields', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
bar Bar @relation(fields: [barId], references: [id]) @allow('read', true)
|
|
barId Int
|
|
@@allow('all', true)
|
|
}
|
|
|
|
model Bar {
|
|
id Int @id @default(autoincrement())
|
|
foos Foo[]
|
|
@@allow('all', true)
|
|
}
|
|
`,
|
|
`Field-level policies are not allowed for relation fields`,
|
|
);
|
|
});
|
|
|
|
it('rejects field-level policy on computed fields', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
x Int
|
|
doubled Int @computed @allow('read', true)
|
|
@@allow('all', true)
|
|
}
|
|
`,
|
|
`Field-level policies are not allowed for computed fields`,
|
|
);
|
|
});
|
|
|
|
it('accepts field-level policy on regular fields', async () => {
|
|
await loadSchema(`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
x Int @allow('read', true)
|
|
y String @deny('update', false)
|
|
@@allow('all', true)
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('accepts complex expressions in field-level policies', async () => {
|
|
await loadSchema(`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model User {
|
|
id Int @id @default(autoincrement())
|
|
email String
|
|
posts Post[]
|
|
@@allow('all', true)
|
|
@@auth()
|
|
}
|
|
|
|
model Post {
|
|
id Int @id @default(autoincrement())
|
|
title String @allow('update', auth() != null && auth().id == authorId)
|
|
author User @relation(fields: [authorId], references: [id])
|
|
authorId Int
|
|
@@allow('all', true)
|
|
}
|
|
`);
|
|
});
|
|
});
|
|
|
|
describe('Field-level @fuzzy attribute', () => {
|
|
it('accepts @fuzzy on a String field with postgres provider', async () => {
|
|
await loadSchema(`
|
|
datasource db {
|
|
provider = 'postgresql'
|
|
url = 'postgresql://localhost/test'
|
|
}
|
|
|
|
model Flavor {
|
|
id Int @id @default(autoincrement())
|
|
name String @fuzzy
|
|
description String? @fuzzy
|
|
}
|
|
`);
|
|
});
|
|
|
|
it('rejects @fuzzy with sqlite provider', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Flavor {
|
|
id Int @id @default(autoincrement())
|
|
name String @fuzzy
|
|
}
|
|
`,
|
|
/`@fuzzy` is only supported for the `postgresql` provider/,
|
|
);
|
|
});
|
|
|
|
it('rejects @fuzzy with mysql provider', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'mysql'
|
|
url = 'mysql://localhost/test'
|
|
}
|
|
|
|
model Flavor {
|
|
id Int @id @default(autoincrement())
|
|
name String @fuzzy
|
|
}
|
|
`,
|
|
/`@fuzzy` is only supported for the `postgresql` provider/,
|
|
);
|
|
});
|
|
|
|
it('rejects @fuzzy on a non-String field', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'postgresql'
|
|
url = 'postgresql://localhost/test'
|
|
}
|
|
|
|
model Flavor {
|
|
id Int @id @default(autoincrement())
|
|
count Int @fuzzy
|
|
}
|
|
`,
|
|
/attribute "@fuzzy" cannot be used on this type of field/,
|
|
);
|
|
});
|
|
});
|
|
|
|
it('requires relation and fk to have consistent optionality', async () => {
|
|
await loadSchemaWithError(
|
|
`
|
|
datasource db {
|
|
provider = 'sqlite'
|
|
url = 'file:./dev.db'
|
|
}
|
|
|
|
model Foo {
|
|
id Int @id @default(autoincrement())
|
|
bar Bar @relation(fields: [barId], references: [id])
|
|
barId Int?
|
|
@@allow('all', true)
|
|
}
|
|
|
|
model Bar {
|
|
id Int @id @default(autoincrement())
|
|
foos Foo[]
|
|
@@allow('all', true)
|
|
}
|
|
`,
|
|
/relation "bar" is not optional/,
|
|
);
|
|
});
|
|
});
|