Skip to content

Commit

Permalink
feat(core): add installOpenapiValidationMiddleware
Browse files Browse the repository at this point in the history
This method allows us to re-use the API server's internal
mechanisms to configure the OpenAPI spec validation in
test cases where we cannot depend on the API server itself
due to circular dependencies.
So this method is designed to be used both by the API server
and the test cases at the same time.

Relationed with hyperledger-cacti#847

Signed-off-by: Elena Izaguirre <e.izaguirre.equiza@accenture.com>
  • Loading branch information
elenaizaguirre authored and petermetz committed Sep 12, 2021
1 parent a01ce63 commit 1f6ea5f
Show file tree
Hide file tree
Showing 57 changed files with 6,034 additions and 330 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Optional } from "typescript-optional";
import { Express } from "express";
import { v4 as uuidv4 } from "uuid";

import OAS from "../../json/openapi.json";

import {
Logger,
Checks,
Expand Down Expand Up @@ -101,6 +103,10 @@ export class CarbonAccountingPlugin
this.instanceId = options.instanceId;
}

public getOpenApiSpec(): unknown {
return OAS;
}

async registerWebServices(app: Express): Promise<IWebServiceEndpoint[]> {
const webServices = await this.getOrCreateWebServices();
webServices.forEach((ws) => ws.registerExpress(app));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Server } from "http";
import type { Server as SecureServer } from "https";
import { Optional } from "typescript-optional";
import { Express } from "express";
import OAS from "../../json/openapi.json";
import {
Logger,
Checks,
Expand Down Expand Up @@ -78,6 +79,10 @@ export class SupplyChainCactusPlugin
this.instanceId = options.instanceId;
}

public getOpenApiSpec(): unknown {
return OAS;
}

async registerWebServices(app: Express): Promise<IWebServiceEndpoint[]> {
const webServices = await this.getOrCreateWebServices();
await Promise.all(webServices.map((ws) => ws.registerExpress(app)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import type {
SetObjectResponseV1,
} from "@hyperledger/cactus-core-api";

import OAS from "../json/openapi.json";

import { GetObjectEndpointV1 } from "./web-services/get-object-endpoint-v1";
import { SetObjectEndpointV1 } from "./web-services/set-object-endpoint-v1";
import { HasObjectEndpointV1 } from "./web-services/has-object-endpoint-v1";
Expand Down Expand Up @@ -76,6 +78,10 @@ export class PluginObjectStoreIpfs implements IPluginObjectStore {
this.log.info(`Created ${this.className}. InstanceID=${opts.instanceId}`);
}

public getOpenApiSpec(): unknown {
return OAS;
}

public async onPluginInit(): Promise<unknown> {
return; // no-op
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cactus-cmd-api-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"express": "4.17.1",
"express-http-proxy": "1.6.2",
"express-jwt": "6.0.0",
"express-openapi-validator": "3.10.0",
"express-openapi-validator": "4.12.12",
"fs-extra": "10.0.0",
"jose": "1.28.1",
"lmify": "0.3.0",
Expand Down
20 changes: 0 additions & 20 deletions packages/cactus-cmd-api-server/src/main/json/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,6 @@
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
}
},
"servers": [
{
"url": "https://www.cactus.stream/{basePath}",
"description": "Public test instance",
"variables": {
"basePath": {
"default": ""
}
}
},
{
"url": "http://localhost:4000/{basePath}",
"description": "Local test instance",
"variables": {
"basePath": {
"default": ""
}
}
}
],
"components": {
"schemas": {
"WatchHealthcheckV1": {
Expand Down
20 changes: 8 additions & 12 deletions packages/cactus-cmd-api-server/src/main/typescript/api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Server as GrpcServer } from "@grpc/grpc-js";
import { ServerCredentials as GrpcServerCredentials } from "@grpc/grpc-js";
import type { Application, Request, Response, RequestHandler } from "express";
import express from "express";
import { OpenApiValidator } from "express-openapi-validator";
import { OpenAPIV3 } from "express-openapi-validator/dist/framework/types";
import compression from "compression";
import bodyParser from "body-parser";
import cors from "cors";
Expand All @@ -35,12 +35,13 @@ import {
} from "@hyperledger/cactus-core-api";

import { PluginRegistry } from "@hyperledger/cactus-core";
import { installOpenapiValidationMiddleware } from "@hyperledger/cactus-core";

import { Logger, LoggerProvider, Servers } from "@hyperledger/cactus-common";

import { ICactusApiServerOptions } from "./config/config-service";
import OAS from "../json/openapi.json";
import { OpenAPIV3 } from "express-openapi-validator/dist/framework/types";
// import { OpenAPIV3 } from "express-openapi-validator/dist/framework/types";

import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter";
import { AuthorizerFactory } from "./authzn/authorizer-factory";
Expand Down Expand Up @@ -606,8 +607,8 @@ export class ApiServer {
this.log.info(`Authorization request handler configured OK.`);
}

const openApiValidator = this.createOpenApiValidator();
await openApiValidator.install(app);
// const openApiValidator = this.createOpenApiValidator();
// await openApiValidator.install(app);

this.getOrCreateWebServices(app); // The API server's own endpoints

Expand All @@ -619,6 +620,9 @@ export class ApiServer {
.map(async (plugin: ICactusPlugin) => {
const p = plugin as IPluginWebService;
await p.getOrCreateWebServices();
const apiSpec = p.getOpenApiSpec() as OpenAPIV3.Document;
if (apiSpec)
await installOpenapiValidationMiddleware({ app, apiSpec, logLevel });
const webSvcs = await p.registerWebServices(app, wsApi);
return webSvcs;
});
Expand Down Expand Up @@ -683,14 +687,6 @@ export class ApiServer {
}
}

createOpenApiValidator(): OpenApiValidator {
return new OpenApiValidator({
apiSpec: OAS as OpenAPIV3.Document,
validateRequests: true,
validateResponses: false,
});
}

createCorsMiddleware(allowedDomains: string[]): RequestHandler {
const allDomainsOk = allowedDomains.includes("*");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Configuration } from "./configuration";
// @ts-ignore
import globalAxios, { AxiosPromise, AxiosInstance } from 'axios';

export const BASE_PATH = "https://www.cactus.stream".replace(/\/+$/, "");
export const BASE_PATH = "http://localhost".replace(/\/+$/, "");

/**
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export class PluginLedgerConnectorStub
this.log.debug(`Instantiated ${this.className} OK`);
}

public getOpenApiSpec(): unknown {
return null;
}

public getInstanceId(): string {
return this.instanceId;
}
Expand Down
27 changes: 7 additions & 20 deletions packages/cactus-core-api/src/main/json/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,6 @@
"url": "https://www.apache.org/licenses/LICENSE-2.0.html"
}
},
"servers": [
{
"url": "https://www.cactus.stream/{basePath}",
"description": "Public test instance",
"variables": {
"basePath": {
"default": ""
}
}
},
{
"url": "http://localhost:4000/{basePath}",
"description": "Local test instance",
"variables": {
"basePath": {
"default": ""
}
}
}
],
"components": {
"schemas": {
"Constants": {
Expand Down Expand Up @@ -426,6 +406,7 @@
"required": [
"key"
],
"additionalProperties": false,
"properties": {
"key": {
"type": "string",
Expand Down Expand Up @@ -464,6 +445,7 @@
"required": [
"key"
],
"additionalProperties": false,
"properties": {
"key": {
"type": "string",
Expand Down Expand Up @@ -507,6 +489,7 @@
"key",
"value"
],
"additionalProperties": false,
"properties": {
"key": {
"type": "string",
Expand Down Expand Up @@ -560,6 +543,7 @@
"key",
"value"
],
"additionalProperties": false,
"properties": {
"key": {
"type": "string",
Expand All @@ -583,6 +567,7 @@
"key",
"value"
],
"additionalProperties": false,
"properties": {
"key": {
"type": "string",
Expand Down Expand Up @@ -635,6 +620,7 @@
"required": [
"key"
],
"additionalProperties": false,
"properties": {
"key": {
"type": "string",
Expand All @@ -650,6 +636,7 @@
"required": [
"key"
],
"additionalProperties": false,
"properties": {
"key": {
"type": "string",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Configuration } from "./configuration";
// @ts-ignore
import globalAxios, { AxiosPromise, AxiosInstance } from 'axios';

export const BASE_PATH = "https://www.cactus.stream".replace(/\/+$/, "");
export const BASE_PATH = "http://localhost".replace(/\/+$/, "");

/**
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface IPluginWebService extends ICactusPlugin {

getHttpServer(): Optional<Server | SecureServer>;
shutdown(): Promise<void>;
getOpenApiSpec(): unknown;
}

export function isIPluginWebService(x: unknown): x is IPluginWebService {
Expand All @@ -26,6 +27,7 @@ export function isIPluginWebService(x: unknown): x is IPluginWebService {
typeof (x as IPluginWebService).getHttpServer === "function" &&
typeof (x as IPluginWebService).getPackageName === "function" &&
typeof (x as IPluginWebService).getInstanceId === "function" &&
typeof (x as IPluginWebService).shutdown === "function"
typeof (x as IPluginWebService).shutdown === "function" &&
typeof (x as IPluginWebService).getOpenApiSpec === "function"
);
}
1 change: 1 addition & 0 deletions packages/cactus-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"@hyperledger/cactus-core-api": "0.9.0",
"express": "4.17.1",
"express-jwt-authz": "2.4.1",
"express-openapi-validator": "4.12.12",
"typescript-optional": "2.0.1"
},
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions packages/cactus-core/src/main/typescript/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ export {
} from "./web-services/authorization-options-provider";

export { consensusHasTransactionFinality } from "./consensus-has-transaction-finality";

export { IInstallOpenapiValidationMiddlewareRequest } from "./web-services/install-open-api-validator-middleware";
export { installOpenapiValidationMiddleware } from "./web-services/install-open-api-validator-middleware";
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { Application, NextFunction, Request, Response } from "express";
import * as OpenApiValidator from "express-openapi-validator";
import { OpenAPIV3 } from "express-openapi-validator/dist/framework/types";

import {
Checks,
LoggerProvider,
LogLevelDesc,
} from "@hyperledger/cactus-common";

export interface IInstallOpenapiValidationMiddlewareRequest {
readonly logLevel: LogLevelDesc;
readonly app: Application;
readonly apiSpec: unknown;
}

/**
* Installs the middleware that validates openapi specifications
* @param app
* @param pluginOAS
*/
export async function installOpenapiValidationMiddleware(
req: IInstallOpenapiValidationMiddlewareRequest,
): Promise<void> {
const fnTag = "installOpenapiValidationMiddleware";
Checks.truthy(req, `${fnTag} req`);
Checks.truthy(req.apiSpec, `${fnTag} req.apiSpec`);
Checks.truthy(req.app, `${fnTag} req.app`);
const { app, apiSpec, logLevel } = req;
const log = LoggerProvider.getOrCreate({
label: fnTag,
level: logLevel || "INFO",
});
log.debug(`Installing validation for OpenAPI specs: `, apiSpec);

const paths = Object.keys((apiSpec as any).paths);
log.debug(`Paths to be ignored: `, paths);

app.use(
OpenApiValidator.middleware({
apiSpec: apiSpec as OpenAPIV3.Document,
validateApiSpec: false,
$refParser: {
mode: "dereference",
},
ignorePaths: (path: string) => !paths.includes(path),
}),
);
app.use(
(
err: {
status?: number;
errors: [
{
path: string;
message: string;
errorCode: string;
},
];
},
req: Request,
res: Response,
next: NextFunction,
) => {
if (err) {
res.status(err.status || 500);
res.send(err.errors);
} else {
next();
}
},
);
}
Loading

0 comments on commit 1f6ea5f

Please sign in to comment.