From 43bcf3793a91e204acf9d07a7ab17a1a2c99211c Mon Sep 17 00:00:00 2001 From: Jim Ezesinachi Date: Thu, 17 Oct 2024 19:24:11 +0000 Subject: [PATCH] fixes & tests Signed-off-by: Jim Ezesinachi --- packages/backend/src/QueryEngine.ts | 52 ++------- packages/backend/src/SynthqlError.ts | 26 ++++- .../backend/src/execution/middleware.test.ts | 2 +- packages/backend/src/execution/middleware.ts | 38 +++++++ .../properties/permissions.test.ts | 107 ++++++++++++++++++ packages/backend/src/tests/queryEngine.ts | 6 +- .../docs/static/reference/assets/search.js | 2 +- 7 files changed, 178 insertions(+), 55 deletions(-) create mode 100644 packages/backend/src/tests/propertyBased/properties/permissions.test.ts diff --git a/packages/backend/src/QueryEngine.ts b/packages/backend/src/QueryEngine.ts index 1cc1e30..1b31bd1 100644 --- a/packages/backend/src/QueryEngine.ts +++ b/packages/backend/src/QueryEngine.ts @@ -1,22 +1,15 @@ import { Pool } from 'pg'; -import { - AnyContext, - AnyQuery, - Query, - QueryResult, - Table, -} from '@synthql/queries'; +import { Query, QueryResult, Table } from '@synthql/queries'; import { QueryPlan, collectLast } from '.'; import { QueryExecutor } from './execution/types'; import { QueryProvider } from './QueryProvider'; import { execute } from './execution/execute'; -import { middleware, Middleware } from './execution/middleware'; +import { Middleware, aclMiddleware } from './execution/middleware'; import { PgExecutor } from './execution/executors/PgExecutor'; import { QueryProviderExecutor } from './execution/executors/QueryProviderExecutor'; import { composeQuery } from './execution/executors/PgExecutor/composeQuery'; import { generateLast } from './util/generators/generateLast'; import { SynthqlError } from './SynthqlError'; -import { mapRecursive } from './util/tree/mapRecursive'; export interface QueryEngineProps { /** @@ -51,7 +44,7 @@ export interface QueryEngineProps { * have the listed permissions in `query.permissions` * passed via the query context permissions list. */ - dangerouslyAllowNoPermissions?: boolean; + dangerouslyIgnorePermissions?: boolean; /** * A list of middlewares that you want to be used to * transform any matching queries, before execution. @@ -128,10 +121,6 @@ export interface QueryEngineProps { logging?: boolean; } -function isQueryWithPermissions(x: any): x is AnyQuery { - return Array.isArray(x?.permissions); -} - export class QueryEngine { private pool: Pool; private schema: string; @@ -148,34 +137,9 @@ export class QueryEngine { connectionString: config.url, max: 10, }); - this.middlewares = [ - ...(config.middlewares ?? []), - middleware({ - predicate: ({ query, context }) => { - const permissions: string[] = []; - - mapRecursive(query, (node) => { - if (isQueryWithPermissions(node)) { - permissions.push(...(node?.permissions ?? [])); - } - - return node; - }); - - if ( - config.dangerouslyAllowNoPermissions || - permissions?.every((item) => - context?.permissions?.includes(item), - ) - ) { - return true; - } else { - throw SynthqlError.createPermissionsError(); - } - }, - transformQuery: ({ query }) => query, - }), - ]; + this.middlewares = config.dangerouslyIgnorePermissions + ? config.middlewares ?? [] + : [...(config.middlewares ?? []), aclMiddleware]; const qpe = new QueryProviderExecutor(config.providers ?? []); this.executors = [ @@ -218,12 +182,12 @@ export class QueryEngine { if ( middleware.predicate({ query, - context: opts?.context, + context: opts?.context ?? {}, }) ) { transformedQuery = middleware.transformQuery({ query: transformedQuery, - context: opts?.context, + context: opts?.context ?? {}, }); } } diff --git a/packages/backend/src/SynthqlError.ts b/packages/backend/src/SynthqlError.ts index 6c76dfb..ee450c2 100644 --- a/packages/backend/src/SynthqlError.ts +++ b/packages/backend/src/SynthqlError.ts @@ -92,7 +92,7 @@ export class SynthqlError extends Error { '', JSON.stringify(query, null, 2), '', - 'Check your query and make sure you have `read` access to all included', + 'Check your query and make sure you have`read` access to all included', 'tables and columns, and have registered all queries via the QueryEngine', ]; @@ -136,16 +136,30 @@ export class SynthqlError extends Error { return new SynthqlError(new Error(), type, lines.join('\n'), 404); } - static createPermissionsError() { + static createPermissionsError({ + node, + missingPermissions, + contextPermissions, + }: { + node: AnyQuery; + missingPermissions: string[]; + contextPermissions: string[]; + }) { const type = 'PermissionsError'; const lines = [ - 'A query with a permissions list (ACL) included,', - 'is missing matching permissions in the', - 'context object permissions list', + `The query '${node?.name}' with a permissions list (ACL) included:`, + `${JSON.stringify(node?.permissions, null, 2)}`, + '', + 'is missing the required matching permissions:', + `${JSON.stringify(missingPermissions, null, 2)}`, + '', + 'in the passed context object permissions list:', + `${JSON.stringify(contextPermissions, null, 2)}`, + '', ]; - return new SynthqlError(new Error(), type, lines.join('\n'), 404); + return new SynthqlError(new Error(), type, lines.join('\n'), 403); } } diff --git a/packages/backend/src/execution/middleware.test.ts b/packages/backend/src/execution/middleware.test.ts index c8584aa..432a434 100644 --- a/packages/backend/src/execution/middleware.test.ts +++ b/packages/backend/src/execution/middleware.test.ts @@ -35,7 +35,7 @@ describe('middleware', async () => { test('Query middleware is correctly executed', async () => { const queryEngine = createQueryEngine({ middlewares: [restrictPaymentsByCustomer], - dangerouslyAllowNoPermissions: false, + dangerouslyIgnorePermissions: false, }); // Create context diff --git a/packages/backend/src/execution/middleware.ts b/packages/backend/src/execution/middleware.ts index eddc6dd..d72a5c5 100644 --- a/packages/backend/src/execution/middleware.ts +++ b/packages/backend/src/execution/middleware.ts @@ -1,4 +1,6 @@ import { AnyContext, AnyQuery } from '@synthql/queries'; +import { mapRecursive } from '../util/tree/mapRecursive'; +import { SynthqlError } from '../SynthqlError'; export interface Middleware< TQuery extends AnyQuery, @@ -47,3 +49,39 @@ export function middleware< transformQuery, }; } + +export const aclMiddleware = middleware({ + predicate: ({ query, context }) => { + const missingPermissions: string[] = []; + + mapRecursive(query, (node) => { + if (isQueryWithPermissions(node)) { + if ( + !(node?.permissions ?? []).every((item) => { + if (context?.permissions?.includes(item)) { + return true; + } else { + missingPermissions.push(item); + return false; + } + }) + ) { + throw SynthqlError.createPermissionsError({ + node, + missingPermissions, + contextPermissions: context?.permissions ?? [], + }); + } + } + + return node; + }); + + return true; + }, + transformQuery: ({ query }) => query, +}); + +function isQueryWithPermissions(x: any): x is AnyQuery { + return Array.isArray(x?.permissions); +} diff --git a/packages/backend/src/tests/propertyBased/properties/permissions.test.ts b/packages/backend/src/tests/propertyBased/properties/permissions.test.ts new file mode 100644 index 0000000..ffafb6e --- /dev/null +++ b/packages/backend/src/tests/propertyBased/properties/permissions.test.ts @@ -0,0 +1,107 @@ +import { describe, expect } from 'vitest'; +import { test } from '@fast-check/vitest'; +import { AnyQuery, Query } from '@synthql/queries'; +import { ArbitraryQueryBuilder } from '../arbitraries/ArbitraryQueryBuilder'; +import { createQueryEngine } from '../../queryEngine'; +import { DB } from '../../generated'; + +const queryBuilder = ArbitraryQueryBuilder.fromPagila(); +const queryEngine = createQueryEngine({ + dangerouslyIgnorePermissions: false, +}); + +// Create type/interface for context +type UserRole = 'user' | 'admin' | 'super'; +type UserPermission = 'user:read' | 'admin:read' | 'super:read'; + +interface Session { + id: number; + email: string; + isActive: boolean; + roles: UserRole[]; + permissions: UserPermission[]; +} + +// Create context +// This would usually be an object generated from a server +// request handler (e.g a parsed cookie/token) +const context: Session = { + id: 1, + email: 'user@example.com', + isActive: true, + roles: ['user', 'admin', 'super'], + permissions: ['user:read', 'admin:read', 'super:read'], +}; + +describe('Property based tests for permissions', () => { + const numRuns = 100; + const timeout = numRuns * 1000; + const endOnFailure = true; + + test.prop( + [queryBuilder.withCardinality('many').withSomeResults().build()], + { + verbose: true, + numRuns, + endOnFailure, + }, + )( + [ + 'A query with permissions will fail unless all permissions are met', + ].join(''), + async (query) => { + const permissionedQuery: Query = { + ...query, + permissions: context.permissions, + name: defaultName(query), + }; + + expect( + async () => await queryEngine.executeAndWait(permissionedQuery), + ).rejects.toThrow( + `The query '${permissionedQuery.name}' with a permissions list (ACL) included:`, + ); + + // Here we check if it errors when some, + // but not all the permissions, are met + expect( + async () => + await queryEngine.executeAndWait(permissionedQuery, { + context: { permissions: [context.permissions[0]] }, + }), + ).rejects.toThrow( + `The query '${permissionedQuery.name}' with a permissions list (ACL) included:`, + ); + }, + timeout, + ); + + test.prop( + [queryBuilder.withCardinality('many').withSomeResults().build()], + { + verbose: true, + numRuns, + endOnFailure, + }, + )( + [ + 'A query with no permissions will never fail for permission issues', + ].join(''), + async (query) => { + expect( + async () => await queryEngine.executeAndWait(query), + ).not.toThrow(); + }, + timeout, + ); +}); + +function defaultName(query: AnyQuery) { + const whereName = Object.keys(query.where).join('-and-'); + + if (whereName === '') { + return `${query.from}-all`; + } + + return `${query.from}-by-${whereName}`; +} diff --git a/packages/backend/src/tests/queryEngine.ts b/packages/backend/src/tests/queryEngine.ts index 398a54d..981cb7b 100644 --- a/packages/backend/src/tests/queryEngine.ts +++ b/packages/backend/src/tests/queryEngine.ts @@ -13,13 +13,13 @@ export const pool = new Pool({ export function createQueryEngine(data?: { middlewares?: Array>; - dangerouslyAllowNoPermissions?: boolean; + dangerouslyIgnorePermissions?: boolean; }) { return new QueryEngine({ pool, schema: 'public', middlewares: data?.middlewares, - dangerouslyAllowNoPermissions: - data?.dangerouslyAllowNoPermissions ?? true, + dangerouslyIgnorePermissions: + data?.dangerouslyIgnorePermissions ?? true, }); } diff --git a/packages/docs/static/reference/assets/search.js b/packages/docs/static/reference/assets/search.js index 9001d09..c77385c 100644 --- a/packages/docs/static/reference/assets/search.js +++ b/packages/docs/static/reference/assets/search.js @@ -1 +1 @@ -window.searchData = "data:application/octet-stream;base64,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"; \ No newline at end of file +window.searchData = "data:application/octet-stream;base64,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"; \ No newline at end of file