From 65bfcb4de5e5d5d42cf3324cbce43216a2c5f987 Mon Sep 17 00:00:00 2001 From: Matt Wonlaw Date: Fri, 3 Jan 2025 11:19:36 -0500 Subject: [PATCH] chore(zql): test nested property access --- .../src/auth/read-authorizer.query.test.ts | 47 +++++++ packages/zero-schema/src/permissions.test.ts | 126 ++++++++++++++++++ .../zql/src/query/query-impl.query.test.ts | 1 + 3 files changed, 174 insertions(+) diff --git a/packages/zero-cache/src/auth/read-authorizer.query.test.ts b/packages/zero-cache/src/auth/read-authorizer.query.test.ts index 9c00566aaa..485a3ec5a0 100644 --- a/packages/zero-cache/src/auth/read-authorizer.query.test.ts +++ b/packages/zero-cache/src/auth/read-authorizer.query.test.ts @@ -260,6 +260,9 @@ const schema = { type AuthData = { sub: string; role: string; + properties?: { + role: string; + }; }; // eslint-disable-next-line arrow-body-style @@ -301,6 +304,7 @@ const permissions = must( isMemberOfProject(authData, eb), isIssueOwner(authData, eb), isIssueCreator(authData, eb), + isAdminThroughNestedData(authData, eb), ); const canSeeComment = ( @@ -312,6 +316,13 @@ const permissions = must( authData: AuthData, {cmpLit}: ExpressionBuilder, ) => cmpLit(authData.role, '=', 'admin'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + type TODO = any; + const isAdminThroughNestedData = ( + authData: AuthData, + {cmpLit}: ExpressionBuilder, + // TODO: proxy should return parameter references instead.... + ) => cmpLit(authData.properties?.role as TODO, 'IS', 'admin'); const isMemberOfProject = ( authData: AuthData, @@ -1446,6 +1457,42 @@ describe('read permissions against nested paths', () => { }); }); +describe('read permissions against nested paths', () => { + beforeEach(() => { + addUser({id: 'owner-creator', name: 'Alice', role: 'user'}); + addUser({id: 'project-member', name: 'Bob', role: 'user'}); + addUser({id: 'not-project-member', name: 'Charlie', role: 'user'}); + + addIssue({ + id: '001', + title: 'Issue 1', + description: 'This is the first issue', + closed: false, + ownerId: 'owner-creator', + creatorId: 'owner-creator', + projectId: '001', + }); + }); + + test('nested property access', () => { + let actual = runReadQueryWithPermissions( + {sub: 'dne', role: '', properties: {role: 'admin'}}, + newQuery(queryDelegate, schema.tables.issue), + ); + expect(toIdsOnly(actual)).toEqual([ + { + id: '001', + }, + ]); + + actual = runReadQueryWithPermissions( + {sub: 'dne', role: ''}, + newQuery(queryDelegate, schema.tables.issue), + ); + expect(toIdsOnly(actual)).toEqual([]); + }); +}); + // maps over nodes, drops all information from `row` except the id // eslint-disable-next-line @typescript-eslint/no-explicit-any function toIdsOnly(nodes: Node[]): any[] { diff --git a/packages/zero-schema/src/permissions.test.ts b/packages/zero-schema/src/permissions.test.ts index bccd5b17c3..6e66466c49 100644 --- a/packages/zero-schema/src/permissions.test.ts +++ b/packages/zero-schema/src/permissions.test.ts @@ -119,3 +119,129 @@ test('permission rules create query ASTs', async () => { } `); }); + +test('nested parameters', async () => { + type AuthData = { + sub: string; + attributes: {role: 'admin' | 'user'}; + }; + const config = await definePermissions( + schema, + () => { + const allowIfAdmin = ( + authData: AuthData, + {cmpLit}: ExpressionBuilder, + ) => cmpLit(authData.attributes.role, '=', 'admin'); + + return { + user: { + row: { + insert: [allowIfAdmin], + update: { + preMutation: [allowIfAdmin], + }, + delete: [allowIfAdmin], + select: [allowIfAdmin], + }, + }, + }; + }, + ); + + expect(config).toMatchInlineSnapshot(` + { + "user": { + "cell": undefined, + "row": { + "delete": [ + [ + "allow", + { + "left": { + "anchor": "authData", + "field": [ + "attributes", + "role", + ], + "type": "static", + }, + "op": "=", + "right": { + "type": "literal", + "value": "admin", + }, + "type": "simple", + }, + ], + ], + "insert": [ + [ + "allow", + { + "left": { + "anchor": "authData", + "field": [ + "attributes", + "role", + ], + "type": "static", + }, + "op": "=", + "right": { + "type": "literal", + "value": "admin", + }, + "type": "simple", + }, + ], + ], + "select": [ + [ + "allow", + { + "left": { + "anchor": "authData", + "field": [ + "attributes", + "role", + ], + "type": "static", + }, + "op": "=", + "right": { + "type": "literal", + "value": "admin", + }, + "type": "simple", + }, + ], + ], + "update": { + "postMutation": undefined, + "preMutation": [ + [ + "allow", + { + "left": { + "anchor": "authData", + "field": [ + "attributes", + "role", + ], + "type": "static", + }, + "op": "=", + "right": { + "type": "literal", + "value": "admin", + }, + "type": "simple", + }, + ], + ], + }, + }, + }, + } + `); +}); diff --git a/packages/zql/src/query/query-impl.query.test.ts b/packages/zql/src/query/query-impl.query.test.ts index 8ecaf77035..93c3f8e266 100644 --- a/packages/zql/src/query/query-impl.query.test.ts +++ b/packages/zql/src/query/query-impl.query.test.ts @@ -7,6 +7,7 @@ import type {AdvancedQuery} from './query-internal.js'; import type {DefaultQueryResultRow} from './query.js'; import {QueryDelegateImpl} from './test/query-delegate.js'; import {issueSchema, userSchema} from './test/testSchemas.js'; +import {toStaticParam} from '../../../zero-protocol/src/ast.js'; /** * Some basic manual tests to get us started.