Skip to content

Commit

Permalink
AN-614: Validates that apiCredentials has a value when securityScheme…
Browse files Browse the repository at this point in the history
… in enabled and it is of type 'apiKey' or 'http'
  • Loading branch information
acenolaza committed May 18, 2022
1 parent 99753bc commit fcaa29e
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 25 deletions.
7 changes: 2 additions & 5 deletions packages/airnode-validator/src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { goSync } from '@api3/promise-utils';
import template from 'lodash/template';
import { z } from 'zod';
import { goSync } from '@api3/promise-utils';
import { SchemaType } from '../types';
import { configSchema } from '../config';
import { Receipt, receiptSchema } from '../receipt';
import { Config, Secrets } from '../types';
import { ValidationResult } from '../validation-result';

type Secrets = Record<string, string | undefined>;
type Config = SchemaType<typeof configSchema>;

/**
* Interpolates `secrets` into `config` and validates the interpolated configuration.
*
Expand Down
74 changes: 71 additions & 3 deletions packages/airnode-validator/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { readFileSync } from 'fs';
import { join } from 'path';
import { ZodError } from 'zod';
import { configSchema, nodeSettingsSchema } from './config';
import { Config, SchemaType } from '../types';
import { version as packageVersion } from '../../package.json';
import { SchemaType } from '../types';

it('successfully parses config.json', () => {
const ois = JSON.parse(
const config = JSON.parse(
readFileSync(join(__dirname, '../../test/fixtures/interpolated-config.valid.json')).toString()
);
expect(() => configSchema.parse(ois)).not.toThrow();
expect(() => configSchema.parse(config)).not.toThrow();
});

describe('nodeSettingsSchema', () => {
Expand Down Expand Up @@ -69,3 +69,71 @@ describe('nodeSettingsSchema', () => {
);
});
});

it('fails if ois.apiSpecifications.security.<securitySchemeName> is defined in ois.apiSpecifications.components.<securitySchemeName> but apiCredentials.securitySchemeValue is empty string', () => {
const config: Config = JSON.parse(
readFileSync(join(__dirname, '../../test/fixtures/interpolated-config.valid.json')).toString()
);

const securitySchemeName = 'Currency Converter Security Scheme';
const securityScheme = {
securitySchemeName: {
in: 'query',
type: 'apiKey',
name: 'access_key',
},
};
const apiCredentials = [
{
oisTitle: 'Currency Converter API',
securitySchemeName: 'Currency Converter Security Scheme',
securitySchemeValue: '${SS_CURRENCY_CONVERTER_API_KEY}',
},
];
const configWithSecuritySchemes = {
...config,
ois: [
...config.ois,
{
...config.ois[0],
apiSpecifications: {
...config.ois[0].apiSpecifications,
components: { securitySchemes: securityScheme },
security: { [securitySchemeName]: [] },
},
},
],
apiCredentials,
};

const invalidConfigEmtpyApiCredentialValue = {
...configWithSecuritySchemes,
apiCredentials: [...config.apiCredentials, { ...apiCredentials[0], securitySchemeValue: '' }],
};
expect(() => configSchema.parse(invalidConfigEmtpyApiCredentialValue)).toThrow(
new ZodError([
{
code: 'custom',
message: 'Security scheme "Currency Converter Security Scheme" is not defined in "components.securitySchemes"',
path: ['ois', 1, 'apiSpecifications', 'security', 0],
},
])
);

const invalidConfigNoApiCredential = {
...configWithSecuritySchemes,
apiCredentials: [
...config.apiCredentials,
{ ...apiCredentials[0], securitySchemeName: apiCredentials[0].securitySchemeName + Date.now().toString() },
],
};
expect(() => configSchema.parse(invalidConfigNoApiCredential)).toThrow(
new ZodError([
{
code: 'custom',
message: 'Security scheme "Currency Converter Security Scheme" is not defined in "components.securitySchemes"',
path: ['ois', 1, 'apiSpecifications', 'security', 0],
},
])
);
});
44 changes: 35 additions & 9 deletions packages/airnode-validator/src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { z } from 'zod';
import { oisSchema } from '../ois';
import { SuperRefinement, z } from 'zod';
import { version as packageVersion } from '../../package.json';
import { oisSchema } from '../ois';
import { ApiCredentials, OIS } from '../types';

export const triggerSchema = z.object({
endpointId: z.string(),
Expand Down Expand Up @@ -138,10 +139,35 @@ export const apiCredentialsSchema = baseApiCredentialsSchema.extend({
oisTitle: z.string(),
});

export const configSchema = z.object({
chains: z.array(chainConfigSchema),
nodeSettings: nodeSettingsSchema,
ois: z.array(oisSchema),
triggers: triggersSchema,
apiCredentials: z.array(apiCredentialsSchema),
});
const validateSecuritySchemesReferences: SuperRefinement<{
ois: OIS[];
apiCredentials: ApiCredentials[];
}> = (config, ctx) => {
config.ois.forEach((ois, index) => {
Object.keys(ois.apiSpecifications.security).forEach((enabledSecuritySchemeName) => {
const enabledSecurityScheme = ois.apiSpecifications.components.securitySchemes[enabledSecuritySchemeName];
if (enabledSecurityScheme && ['apiKey', 'http'].includes(enabledSecurityScheme.type)) {
const securitySchemeApiCredentials = config.apiCredentials.find(
(apiCredentials) => apiCredentials.securitySchemeName === enabledSecuritySchemeName
);
if (!securitySchemeApiCredentials || !securitySchemeApiCredentials.securitySchemeValue) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `The security scheme is enabled but no credentials are provided`,
path: ['ois', index, 'apiSpecifications', 'security', enabledSecuritySchemeName],
});
}
}
});
});
};

export const configSchema = z
.object({
chains: z.array(chainConfigSchema),
nodeSettings: nodeSettingsSchema,
ois: z.array(oisSchema),
triggers: triggersSchema,
apiCredentials: z.array(apiCredentialsSchema),
})
.superRefine(validateSecuritySchemesReferences);
14 changes: 7 additions & 7 deletions packages/airnode-validator/src/ois/ois.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { z } from 'zod';
import intersection from 'lodash/intersection';
import forEach from 'lodash/forEach';
import intersection from 'lodash/intersection';
import trimEnd from 'lodash/trimEnd';
import trimStart from 'lodash/trimStart';
import { SchemaType, ValidatorRefinement } from '../types';
import { z } from 'zod';
import { OIS, SchemaType, ValidatorRefinement } from '../types';

function removeBraces(value: string) {
return trimEnd(trimStart(value, '{'), '}');
Expand Down Expand Up @@ -161,11 +161,11 @@ export const apiSpecificationSchema = z
servers: z.array(serverSchema),
security: z.record(z.tuple([])),
})
.superRefine((apiSpecification, ctx) => {
Object.keys(apiSpecification.security).forEach((enabledSecuritySchemeName, index) => {
.superRefine((apiSpecifications, ctx) => {
Object.keys(apiSpecifications.security).forEach((enabledSecuritySchemeName, index) => {
// Verify that ois.apiSpecifications.security.<securitySchemeName> is
// referencing a valid ois.apiSpecifications.components.<securitySchemeName> object
const enabledSecurityScheme = apiSpecification.components.securitySchemes[enabledSecuritySchemeName];
const enabledSecurityScheme = apiSpecifications.components.securitySchemes[enabledSecuritySchemeName];
if (!enabledSecurityScheme) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
Expand Down Expand Up @@ -218,5 +218,5 @@ export const baseOisSchema = z.object({
apiSpecifications: apiSpecificationSchema,
endpoints: z.array(endpointSchema),
});
type OIS = SchemaType<typeof baseOisSchema>;

export const oisSchema = baseOisSchema.superRefine(ensureSingleParameterUsagePerEndpoint);
10 changes: 9 additions & 1 deletion packages/airnode-validator/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { ZodFirstPartySchemaTypes, RefinementCtx, z } from 'zod';
import { RefinementCtx, z, ZodFirstPartySchemaTypes } from 'zod';
import { apiCredentialsSchema, configSchema } from './config';
import { oisSchema } from './ois';

export type SchemaType<Schema extends ZodFirstPartySchemaTypes> = z.infer<Schema>;

export type ValidatorRefinement<T> = (arg: T, ctx: RefinementCtx) => void;

export type Secrets = Record<string, string | undefined>;

export type Config = SchemaType<typeof configSchema>;
export type OIS = SchemaType<typeof oisSchema>;
export type ApiCredentials = SchemaType<typeof apiCredentialsSchema>;

0 comments on commit fcaa29e

Please sign in to comment.