mirror of
https://github.com/zenstackhq/zenstack
synced 2026-05-24 10:08:55 +00:00
Fixed issue where access policy rules couldn't reference fields inherited from mixins defined in separate imported files. The language service now correctly resolves these fields during scope computation. ## Root Cause The `getRecursiveBases()` function only searched for mixin declarations in the current document (`decl.$container.declarations`), which failed for imported mixins. ## Solution - Modified `getRecursiveBases()` to accept optional `LangiumDocuments` parameter - Implemented two-strategy approach: 1. Use resolved reference if available (post-linking) 2. Search by name across all documents including imports (pre-linking) - Updated `ZModelScopeComputation.processNode()` to pass `LangiumDocuments` - Leverages existing `getAllDeclarationsIncludingImports()` helper ## Changes - **packages/language/src/utils.ts**: Fixed `getRecursiveBases()` to search imported documents - **packages/language/src/zmodel-scope.ts**: Pass LangiumDocuments to scope computation - **packages/language/test/mixin.test.ts**: Added tests for imported mixin field resolution - **packages/testtools**: Added `extraZModelFiles` option for multi-file test schemas - **tests/regression/test/issue-598.test.ts**: Regression test for the issue ## Test Results ✅ All language package tests pass (65 tests) ✅ Regression test validates policy rules can access imported mixin fields ✅ Handles edge cases: cyclic imports, nested mixins, transitive imports Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
79 lines
2.2 KiB
TypeScript
79 lines
2.2 KiB
TypeScript
import { createPolicyTestClient } from '@zenstackhq/testtools';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
describe('Regression for issue 598', () => {
|
|
it('access policy can reference mixin fields from imported files', async () => {
|
|
const db = await createPolicyTestClient(
|
|
`
|
|
import './mixins'
|
|
|
|
datasource db {
|
|
provider = 'postgresql'
|
|
url = '$DB_URL'
|
|
}
|
|
|
|
model User {
|
|
id Int @id @default(autoincrement())
|
|
email String @unique
|
|
documents Document[]
|
|
|
|
@@allow('all', true)
|
|
}
|
|
|
|
model Document with AuditMixin {
|
|
id String @id @default(cuid())
|
|
title String
|
|
ownerId Int
|
|
owner User @relation(fields: [ownerId], references: [id])
|
|
|
|
@@allow('create,read', auth() != null)
|
|
@@allow('update', auth().id == createdById) // createdById from mixin
|
|
}
|
|
`,
|
|
{
|
|
extraZModelFiles: {
|
|
mixins: `
|
|
type AuditMixin {
|
|
createdById Int
|
|
createdAt DateTime @default(now())
|
|
}
|
|
`,
|
|
},
|
|
},
|
|
);
|
|
|
|
// Test that the policy rule using mixin field works correctly
|
|
const userDb = db.$setAuth({ id: 1 });
|
|
const otherUserDb = db.$setAuth({ id: 2 });
|
|
|
|
// Create users first
|
|
await db.user.create({ data: { id: 1, email: 'user1@test.com' } });
|
|
await db.user.create({ data: { id: 2, email: 'user2@test.com' } });
|
|
|
|
// Create document as user 1
|
|
await userDb.document.create({
|
|
data: {
|
|
id: 'doc-1',
|
|
title: 'Test Document',
|
|
ownerId: 1,
|
|
createdById: 1, // From mixin
|
|
},
|
|
});
|
|
|
|
// User 1 should be able to update (matches createdById)
|
|
await expect(
|
|
userDb.document.update({
|
|
where: { id: 'doc-1' },
|
|
data: { title: 'Updated' },
|
|
}),
|
|
).toResolveTruthy();
|
|
|
|
// User 2 should NOT be able to update (different createdById)
|
|
await expect(
|
|
otherUserDb.document.update({
|
|
where: { id: 'doc-1' },
|
|
data: { title: 'Hacked' },
|
|
}),
|
|
).toBeRejectedNotFound();
|
|
});
|
|
});
|