Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow authorization token for JsonApiCondition to support endpoints requiring OAuth, JWT authorization #599

Merged
merged 5 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/shared/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
export const BlockIdentifierSchema = z.union([
BlockNumber,
BlockHash,
BlockTag,
]);
5 changes: 3 additions & 2 deletions packages/shared/test/schemas.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -46,7 +46,8 @@ describe('block identifier address schema', () => {
});

it('should accept valid block hashes', () => {
const validBlockHash = '0x1234567890123456789012345678901234567890123456789012345678901234';
const validBlockHash =
'0x1234567890123456789012345678901234567890123456789012345678901234';
BlockIdentifierSchema.parse(validBlockHash);
});

Expand Down
9 changes: 8 additions & 1 deletion packages/taco/src/conditions/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ':';

Expand Down
4 changes: 2 additions & 2 deletions packages/taco/src/conditions/context/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 4 additions & 2 deletions packages/taco/src/conditions/schemas/context.ts
Original file line number Diff line number Diff line change
@@ -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()]);

Expand Down
10 changes: 10 additions & 0 deletions packages/taco/src/conditions/schemas/json-api.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,6 +34,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
});

Expand Down
11 changes: 7 additions & 4 deletions packages/taco/src/conditions/schemas/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand All @@ -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
});
Expand Down
60 changes: 60 additions & 0 deletions packages/taco/test/conditions/base/json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -39,6 +40,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;
Expand All @@ -62,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);
});
});
});
});
17 changes: 10 additions & 7 deletions packages/taco/test/conditions/base/rpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
});
});

Expand All @@ -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'],
});
});

Expand Down Expand Up @@ -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)',
],
},
});
});
Expand Down