From b3ed1c4ab7ceb9ae72411cc8b2319af69dc955b8 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 28 Oct 2024 09:31:15 -0400 Subject: [PATCH 1/5] Allow authorization token to be optionally specified for JsonApiCondition. The value for an authorization token must be a context variable. --- packages/taco/src/conditions/schemas/json-api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/taco/src/conditions/schemas/json-api.ts b/packages/taco/src/conditions/schemas/json-api.ts index 4a9afb82..0bc20e0a 100644 --- a/packages/taco/src/conditions/schemas/json-api.ts +++ b/packages/taco/src/conditions/schemas/json-api.ts @@ -25,6 +25,7 @@ export const jsonApiConditionSchema = z.object({ endpoint: z.string().url(), parameters: z.record(z.string(), z.unknown()).optional(), query: jsonPathSchema.optional(), + authorizationToken: contextParamSchema.optional(), returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods }); From 9527c42aca04a6a7ccc687d0378698beff1b15f9 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 28 Oct 2024 09:33:24 -0400 Subject: [PATCH 2/5] Allow query for JsonApiCondition to contain context variables. There is a difference between checking that an entire string is a context variable, and checking whether a context variable is a substring within a string - make that difference clearer with respect to regexp constants. --- packages/taco/src/conditions/const.ts | 9 ++++++++- packages/taco/src/conditions/context/context.ts | 4 ++-- packages/taco/src/conditions/schemas/context.ts | 6 ++++-- packages/taco/src/conditions/schemas/json-api.ts | 9 +++++++++ 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/taco/src/conditions/const.ts b/packages/taco/src/conditions/const.ts index 4cb67d6c..82370ca0 100644 --- a/packages/taco/src/conditions/const.ts +++ b/packages/taco/src/conditions/const.ts @@ -5,7 +5,14 @@ import { } from '@nucypher/taco-auth'; // Only allow alphanumeric characters and underscores -export const CONTEXT_PARAM_REGEXP = new RegExp('^:[a-zA-Z_][a-zA-Z0-9_]*$'); +const contextParamRegexString = ':[a-zA-Z_][a-zA-Z0-9_]*'; + +export const CONTEXT_PARAM_REGEXP = new RegExp(contextParamRegexString); + +// Entire string is context param +export const CONTEXT_PARAM_FULL_MATCH_REGEXP = new RegExp( + `^${contextParamRegexString}$`, +); export const CONTEXT_PARAM_PREFIX = ':'; diff --git a/packages/taco/src/conditions/context/context.ts b/packages/taco/src/conditions/context/context.ts index 8b4992b8..116d04af 100644 --- a/packages/taco/src/conditions/context/context.ts +++ b/packages/taco/src/conditions/context/context.ts @@ -14,8 +14,8 @@ import { CompoundConditionType } from '../compound-condition'; import { Condition, ConditionProps } from '../condition'; import { ConditionExpression } from '../condition-expr'; import { + CONTEXT_PARAM_FULL_MATCH_REGEXP, CONTEXT_PARAM_PREFIX, - CONTEXT_PARAM_REGEXP, USER_ADDRESS_PARAMS, } from '../const'; @@ -138,7 +138,7 @@ export class ConditionContext { } private static isContextParameter(param: unknown): boolean { - return !!String(param).match(CONTEXT_PARAM_REGEXP); + return !!String(param).match(CONTEXT_PARAM_FULL_MATCH_REGEXP); } private static findContextParameters(condition: ConditionProps) { diff --git a/packages/taco/src/conditions/schemas/context.ts b/packages/taco/src/conditions/schemas/context.ts index 3c2f2a92..1b3208fc 100644 --- a/packages/taco/src/conditions/schemas/context.ts +++ b/packages/taco/src/conditions/schemas/context.ts @@ -1,10 +1,12 @@ import { z } from 'zod'; -import { CONTEXT_PARAM_REGEXP } from '../const'; +import { CONTEXT_PARAM_FULL_MATCH_REGEXP } from '../const'; import { plainStringSchema } from './common'; -export const contextParamSchema = z.string().regex(CONTEXT_PARAM_REGEXP); +export const contextParamSchema = z + .string() + .regex(CONTEXT_PARAM_FULL_MATCH_REGEXP); const paramSchema = z.union([plainStringSchema, z.boolean(), z.number()]); diff --git a/packages/taco/src/conditions/schemas/json-api.ts b/packages/taco/src/conditions/schemas/json-api.ts index 0bc20e0a..626a4c15 100644 --- a/packages/taco/src/conditions/schemas/json-api.ts +++ b/packages/taco/src/conditions/schemas/json-api.ts @@ -1,11 +1,20 @@ import { JSONPath } from '@astronautlabs/jsonpath'; import { z } from 'zod'; +import { CONTEXT_PARAM_REGEXP } from '../const'; + +import { contextParamSchema } from './context'; import { returnValueTestSchema } from './return-value-test'; export const JsonApiConditionType = 'json-api'; const validateJSONPath = (jsonPath: string): boolean => { + // account for embedded context variables + if (CONTEXT_PARAM_REGEXP.test(jsonPath)) { + // skip validation + return true; + } + try { JSONPath.parse(jsonPath); return true; From 521d56ebb53b23f7ace939e8e6e678ed30e8a02f Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 28 Oct 2024 09:35:19 -0400 Subject: [PATCH 3/5] Add tests for authorization token property for JsonApiCondition. --- .../taco/test/conditions/base/json.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/taco/test/conditions/base/json.test.ts b/packages/taco/test/conditions/base/json.test.ts index 2f9291d4..5f8ea045 100644 --- a/packages/taco/test/conditions/base/json.test.ts +++ b/packages/taco/test/conditions/base/json.test.ts @@ -39,6 +39,39 @@ describe('JsonApiCondition', () => { }); }); + describe('authorizationToken', () => { + it('accepts context variable', () => { + const result = JsonApiCondition.validate(jsonApiConditionSchema, { + ...testJsonApiConditionObj, + authorizationToken: ':authToken', + }); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual({ + ...testJsonApiConditionObj, + authorizationToken: ':authToken', + }); + }); + it.each([ + 'authToken', + 'ABCDEF1234567890', + ':authToken?', + '$:authToken', + ':auth-Token', + ])('rejects invalid context variable', (contextVar) => { + const result = JsonApiCondition.validate(jsonApiConditionSchema, { + ...testJsonApiConditionObj, + authorizationToken: `${contextVar}`, + }); + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + expect(result.error?.format()).toMatchObject({ + authorizationToken: { + _errors: ['Invalid'], + }, + }); + }); + }); + describe('parameters', () => { it('accepts conditions without query path', () => { const { query, ...noQueryObj } = testJsonApiConditionObj; From 6d4babe7bb5ea5df8397c814d362a204d9216116 Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 28 Oct 2024 09:35:55 -0400 Subject: [PATCH 4/5] Add test for context variables within various properties of a JsonApiCondition. --- .../taco/test/conditions/base/json.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/taco/test/conditions/base/json.test.ts b/packages/taco/test/conditions/base/json.test.ts index 5f8ea045..cc04fe31 100644 --- a/packages/taco/test/conditions/base/json.test.ts +++ b/packages/taco/test/conditions/base/json.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'; import { JsonApiCondition, jsonApiConditionSchema, + JsonApiConditionType, } from '../../../src/conditions/base/json-api'; import { testJsonApiConditionObj } from '../../test-utils'; @@ -95,5 +96,31 @@ describe('JsonApiCondition', () => { expect(result.data).toEqual(noParamsObj); }); }); + + describe('context variables', () => { + it('allow context variables for various values including as substring', () => { + const jsonApiConditionObj = { + conditionType: JsonApiConditionType, + endpoint: + 'https://api.coingecko.com/api/:version/simple/:endpointPath', + parameters: { + ids: 'ethereum', + vs_currencies: ':vsCurrency', + }, + query: 'ethereum.:vsCurrency', + authorizationToken: ':authToken', + returnValueTest: { + comparator: '==', + value: ':expectedPrice', + }, + }; + const result = JsonApiCondition.validate( + jsonApiConditionSchema, + jsonApiConditionObj, + ); + expect(result.error).toBeUndefined(); + expect(result.data).toEqual(jsonApiConditionObj); + }); + }); }); }); From 0e25a114efcdf00ef8ed55bd35bfed9bce81099e Mon Sep 17 00:00:00 2001 From: derekpierre Date: Mon, 28 Oct 2024 09:37:30 -0400 Subject: [PATCH 5/5] Fix linting. --- packages/shared/src/schemas.ts | 6 +++++- packages/shared/test/schemas.test.ts | 5 +++-- packages/taco/src/conditions/schemas/rpc.ts | 11 +++++++---- packages/taco/test/conditions/base/rpc.test.ts | 17 ++++++++++------- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/shared/src/schemas.ts b/packages/shared/src/schemas.ts index 1ca394a3..37dfa265 100644 --- a/packages/shared/src/schemas.ts +++ b/packages/shared/src/schemas.ts @@ -18,4 +18,8 @@ const BlockNumber = z.number().int().nonnegative(); const BlockHash = z.string().regex(BLOCK_HASH_REGEXP, 'Invalid block hash'); const BlockTag = z.enum(['earliest', 'finalized', 'safe', 'latest', 'pending']); -export const BlockIdentifierSchema = z.union([BlockNumber, BlockHash, BlockTag]); \ No newline at end of file +export const BlockIdentifierSchema = z.union([ + BlockNumber, + BlockHash, + BlockTag, +]); diff --git a/packages/shared/test/schemas.test.ts b/packages/shared/test/schemas.test.ts index 86bc7dae..7b180394 100644 --- a/packages/shared/test/schemas.test.ts +++ b/packages/shared/test/schemas.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { EthAddressSchema, BlockIdentifierSchema } from '../src'; +import { BlockIdentifierSchema, EthAddressSchema } from '../src'; describe('ethereum address schema', () => { it('should accept valid ethereum address', () => { @@ -46,7 +46,8 @@ describe('block identifier address schema', () => { }); it('should accept valid block hashes', () => { - const validBlockHash = '0x1234567890123456789012345678901234567890123456789012345678901234'; + const validBlockHash = + '0x1234567890123456789012345678901234567890123456789012345678901234'; BlockIdentifierSchema.parse(validBlockHash); }); diff --git a/packages/taco/src/conditions/schemas/rpc.ts b/packages/taco/src/conditions/schemas/rpc.ts index 771c4d2f..a80a5956 100644 --- a/packages/taco/src/conditions/schemas/rpc.ts +++ b/packages/taco/src/conditions/schemas/rpc.ts @@ -15,9 +15,12 @@ export const RpcConditionType = 'rpc'; const EthAddressOrContextVariableSchema = z.union([ EthAddressSchema, UserAddressSchema, - contextParamSchema + contextParamSchema, +]); +const BlockOrContextParamSchema = z.union([ + BlockIdentifierSchema, + contextParamSchema, ]); -const BlockOrContextParamSchema = z.union([BlockIdentifierSchema, contextParamSchema]) // eth_getBalance schema specification // - Ethereum spec: https://ethereum.github.io/execution-apis/api-documentation/ @@ -29,8 +32,8 @@ export const rpcConditionSchema = baseConditionSchema.extend({ parameters: z.union([ // Spec requires 2 parameters: an address and a block identifier z.tuple([EthAddressOrContextVariableSchema, BlockOrContextParamSchema]), - // Block identifier can be omitted, since web3py (which runs on TACo exec layer) defaults to 'latest', - z.tuple([EthAddressOrContextVariableSchema]), + // Block identifier can be omitted, since web3py (which runs on TACo exec layer) defaults to 'latest', + z.tuple([EthAddressOrContextVariableSchema]), ]), returnValueTest: returnValueTestSchema, // Update to allow multiple return values after expanding supported methods }); diff --git a/packages/taco/test/conditions/base/rpc.test.ts b/packages/taco/test/conditions/base/rpc.test.ts index 35c0d22b..e1cb99cb 100644 --- a/packages/taco/test/conditions/base/rpc.test.ts +++ b/packages/taco/test/conditions/base/rpc.test.ts @@ -63,26 +63,26 @@ describe('validation', () => { it('accepts a single UserAddress as address', () => { const result = RpcCondition.validate(rpcConditionSchema, { ...testRpcConditionObj, - parameters: [":userAddress"], + parameters: [':userAddress'], }); expect(result.error).toBeUndefined(); expect(result.data).toEqual({ ...testRpcConditionObj, - parameters: [":userAddress"], + parameters: [':userAddress'], }); }); it('accepts a single context variable as address', () => { const result = RpcCondition.validate(rpcConditionSchema, { ...testRpcConditionObj, - parameters: [":testContextVar"], + parameters: [':testContextVar'], }); expect(result.error).toBeUndefined(); expect(result.data).toEqual({ ...testRpcConditionObj, - parameters: [":testContextVar"], + parameters: [':testContextVar'], }); }); @@ -102,13 +102,13 @@ describe('validation', () => { it('accepts context params for address and block number', () => { const result = RpcCondition.validate(rpcConditionSchema, { ...testRpcConditionObj, - parameters: [":testAddress", ":testBlockNumber"], + parameters: [':testAddress', ':testBlockNumber'], }); expect(result.error).toBeUndefined(); expect(result.data).toEqual({ ...testRpcConditionObj, - parameters: [":testAddress", ":testBlockNumber"], + parameters: [':testAddress', ':testBlockNumber'], }); }); @@ -143,7 +143,10 @@ describe('validation', () => { expect(result.data).toBeUndefined(); expect(result.error?.format()).toMatchObject({ parameters: { - _errors: ['Array must contain at least 2 element(s)', 'Array must contain at least 1 element(s)'], + _errors: [ + 'Array must contain at least 2 element(s)', + 'Array must contain at least 1 element(s)', + ], }, }); });