lib/liveValidation/operationSearcher.ts (305 lines of code) (raw):

import { ParsedUrlQuery } from "querystring"; import { LiveValidationError } from "../models"; import { LowerHttpMethods, Operation, SwaggerSpec } from "../swagger/swaggerTypes"; import { RegExpWithKeys } from "../transform/pathRegexTransformer"; import { traverseSwagger } from "../transform/traverseSwagger"; import { ErrorCodes, knownTitleToResourceProviders, unknownApiVersion, unknownResourceProvider, } from "../util/constants"; import { getOavErrorMeta } from "../util/errorDefinitions"; import { getProviderFromPathTemplate, Writable } from "../util/utils"; import { LiveValidatorLoggingLevels, LiveValidatorLoggingTypes } from "./liveValidator"; import { ValidationRequest } from "./operationValidator"; export interface OperationMatch { operation: Operation; pathRegex: RegExpWithKeys; pathMatch: RegExpExecArray; } interface PotentialOperationsResult { readonly matches: OperationMatch[]; readonly resourceProvider: string; readonly apiVersion: string; readonly reason?: LiveValidationError; } // Key http method, e.g. get type ApiVersion = Map<LowerHttpMethods, Operation[]>; // Key api-version, e.g. 2020-01-01 type Provider = Map<string, ApiVersion>; export class OperationSearcher { // Key provider, e.g. Microsoft.ApiManagement public readonly cache = new Map<string, Provider>(); public constructor( private logging: ( message: string, level?: LiveValidatorLoggingLevels, loggingType?: LiveValidatorLoggingTypes, operationName?: string, durationInMilliseconds?: number, validationRequest?: ValidationRequest ) => void ) {} public addSpecToCache(spec: SwaggerSpec) { traverseSwagger(spec, { onOperation: (operation, path, method) => { const httpMethod = method.toLowerCase() as LowerHttpMethods; const pathObject = path; const pathStr = pathObject._pathTemplate; let apiVersion = spec.info.version; let provider = getProviderFromPathTemplate(pathStr); const addOperationToCache = () => { let apiVersions = this.cache.get(provider!); if (apiVersions === undefined) { apiVersions = new Map(); this.cache.set(provider!, apiVersions); } let allMethods = apiVersions.get(apiVersion); if (allMethods === undefined) { allMethods = new Map(); apiVersions.set(apiVersion, allMethods); } let operationsForHttpMethod = allMethods.get(httpMethod); if (operationsForHttpMethod === undefined) { operationsForHttpMethod = []; allMethods.set(httpMethod, operationsForHttpMethod); } operationsForHttpMethod.push(operation); }; this.logging( `${apiVersion}, ${operation.operationId}, ${pathStr}, ${httpMethod}`, LiveValidatorLoggingLevels.debug ); if (!apiVersion) { this.logging( `Unable to find apiVersion for path : "${pathObject._pathTemplate}".`, LiveValidatorLoggingLevels.error, LiveValidatorLoggingTypes.specTrace, "Oav.OperationSearcher.addSpecToCache", undefined, { providerNamespace: spec._providerNamespace ?? "unknown", apiVersion: spec.info.version, specName: spec._filePath, } ); apiVersion = unknownApiVersion; } apiVersion = apiVersion.toLowerCase(); if (!provider) { const title = spec.info.title; // Whitelist lookups: Look up knownTitleToResourceProviders // Putting the provider namespace onto operation for future use if (title && knownTitleToResourceProviders[title]) { operation.provider = knownTitleToResourceProviders[title]; } // Put the operation into 'Microsoft.Unknown' RPs provider = unknownResourceProvider; this.logging( `Unable to find provider for path : "${pathObject._pathTemplate}". ` + `Bucketizing into provider: "${provider}"`, LiveValidatorLoggingLevels.warn, LiveValidatorLoggingTypes.specTrace, "Oav.OperationSearcher.addSpecToCache", undefined, { providerNamespace: spec._providerNamespace ?? "unknown", apiVersion: spec.info.version, specName: spec._filePath, } ); } provider = provider.toLowerCase(); addOperationToCache(); }, }); } /** * Gets the swagger operation based on the HTTP url and method */ public search(info: ValidationRequest): { operationMatch: OperationMatch; apiVersion: string; } { const startTime = Date.now(); const requestInfo = { ...info }; const searchOperation = () => { const operations = this.getPotentialOperations(requestInfo); if (operations.reason !== undefined) { this.logging( `${operations.reason.message} with requestUrl ${requestInfo.requestUrl}`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.trace, "Oav.OperationSearcher.search.getPotentialOperations", undefined, requestInfo ); } return operations; }; let potentialOperations = searchOperation(); const firstReason = potentialOperations.reason; if (potentialOperations!.matches.length === 0) { this.logging( `Fallback to ${unknownResourceProvider} -> ${unknownApiVersion}`, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.trace, "Oav.OperationSearcher.search", undefined, requestInfo ); //requestInfo.apiVersion = unknownApiVersion; potentialOperations = searchOperation(); } if (potentialOperations.matches.length === 0) { throw firstReason ?? potentialOperations.reason; } if (potentialOperations.matches.length > 1) { const operationInfos: Array<{ id: string; path: string; specPath: string }> = []; potentialOperations.matches.forEach(({ operation }) => { const specPath = operation._path._spec._filePath; operationInfos.push({ id: operation.operationId!, path: operation._path._pathTemplate, specPath, }); }); const msg = `Found multiple matching operations ` + `for request url "${requestInfo.requestUrl}" with HTTP Method "${requestInfo.requestMethod}".` + `Operation Information: ${JSON.stringify(operationInfos)}`; this.logging( msg, LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.trace, "Oav.OperationSearcher.Search", undefined, requestInfo ); const e = new LiveValidationError(ErrorCodes.MultipleOperationsFound.name, msg); throw e; } this.logging( "Complete operation search", LiveValidatorLoggingLevels.info, LiveValidatorLoggingTypes.perfTrace, "Oav.OperationSearcher.Search", Date.now() - startTime, requestInfo ); return { operationMatch: potentialOperations.matches[0], apiVersion: potentialOperations.apiVersion, }; } /** * Gets list of potential operations objects for given url and method. * * @param requestInfo The parsed request info for which to find potential operations. * * @returns Potential operation result object. */ public getPotentialOperations(requestInfo: ValidationRequest): PotentialOperationsResult { if (this.cache.size === 0) { const msgStr = `Please call "liveValidator.initialize()" before calling this method, ` + `so that cache is populated.`; throw new Error(msgStr); } const ret: Writable<PotentialOperationsResult> = { matches: [], resourceProvider: requestInfo.providerNamespace, apiVersion: requestInfo.apiVersion, }; if (requestInfo.pathStr === "") { ret.reason = new LiveValidationError( ErrorCodes.PathNotFoundInRequestUrl.name, `Could not find path from requestUrl: "${requestInfo.requestUrl}".` ); return ret; } // Search using provider const allApiVersions = this.cache.get(requestInfo.providerNamespace); let meta; if (allApiVersions === undefined) { // provider does not exist in cache meta = getOavErrorMeta(ErrorCodes.OperationNotFoundInCacheWithProvider.name as any, { providerNamespace: requestInfo.providerNamespace, }); ret.reason = new LiveValidationError( ErrorCodes.OperationNotFoundInCacheWithProvider.name, meta.message ); return ret; } // Search using api-version found in the requestUrl if (!requestInfo.apiVersion) { ret.reason = new LiveValidationError( ErrorCodes.OperationNotFoundInCacheWithApi.name, `Could not find api-version in requestUrl "${requestInfo.requestUrl}".` ); return ret; } const allMethods = allApiVersions.get(requestInfo.apiVersion); if (allMethods === undefined) { meta = getOavErrorMeta(ErrorCodes.OperationNotFoundInCacheWithApi.name as any, { apiVersion: requestInfo.apiVersion, providerNamespace: requestInfo.providerNamespace, }); ret.reason = new LiveValidationError( ErrorCodes.OperationNotFoundInCacheWithApi.name, meta.message ); return ret; } const operationsForHttpMethod = allMethods?.get(requestInfo.requestMethod!); // Search using requestMethod provided by user if (operationsForHttpMethod === undefined) { meta = getOavErrorMeta(ErrorCodes.OperationNotFoundInCacheWithVerb.name as any, { requestMethod: requestInfo.requestMethod, apiVersion: requestInfo.apiVersion, providerNamespace: requestInfo.providerNamespace, }); ret.reason = new LiveValidationError( ErrorCodes.OperationNotFoundInCacheWithVerb.name, meta.message ); return ret; } // Find the best match using regex on path ret.matches = getMatchedOperations( requestInfo.host!, requestInfo.pathStr!, operationsForHttpMethod, requestInfo.query ); if (ret.matches.length === 0 && ret.reason === undefined) { meta = getOavErrorMeta(ErrorCodes.OperationNotFoundInCache.name as any, { requestMethod: requestInfo.requestMethod, apiVersion: requestInfo.apiVersion, providerNamespace: requestInfo.providerNamespace, }); ret.reason = new LiveValidationError(ErrorCodes.OperationNotFoundInCache.name, meta.message); } return ret; } } /** * Gets list of matched operations objects for given url. * * @param {string} requestUrl The url for which to find matched operations. * * @param {Array<Operation>} operations The list of operations to search. * * @returns {Array<Operation>} List of matched operations with the request url. */ const getMatchedOperations = ( host: string, pathStr: string, operations: Operation[], query?: ParsedUrlQuery ): OperationMatch[] => { const queryMatchResult: OperationMatch[] = []; // Priority 0 const result: OperationMatch[] = []; // Priority 1 const multiParamResult: OperationMatch[] = []; // Priority 2 for (const operation of operations) { const path = operation._path; // Validate query first so we could match operation in x-ms-paths const queryMatch = path._validateQuery === undefined ? undefined : path._validateQuery({ isResponse: false }, query).length === 0; if (queryMatch === false) { continue; } const toMatch = path._pathRegex._hostTemplate ? host + pathStr : pathStr; const pathMatch = path._pathRegex.exec(toMatch); if (pathMatch === null) { continue; } (queryMatch !== undefined ? queryMatchResult : path._pathRegex._hasMultiPathParam ? multiParamResult : result ).push({ operation, pathRegex: path._pathRegex, pathMatch, }); } return queryMatchResult.length > 0 ? queryMatchResult : result.length > 0 ? result : multiParamResult; };