kogosoftwarellc / open-api

A Monorepo of various packages to power OpenAPI in node
MIT License
895 stars 237 forks source link

Unable to use OpenAPI v3 file #700

Closed noaoh closed 3 years ago

noaoh commented 3 years ago

Hello there! I am using the express-openapi module v7.2.0 on nodejs v12.18.2. The OS of the computer I am running it on is Windows Server 2019 Datacenter edition. Whenever I attempt to use an OpenAPI v3 yaml file, express-openapi throws validation errors that shows it is validating it against the swagger v2 specification. This is my debug file: 2020-12-21T21_54_01_145Z-debug.log Here is my yaml file:

---
openapi: 3.0.1
info:
  title: Lorem Ipsum 
  description: Lorem Ipsum
  version: 1.0.0
servers:
- url: /info/api
tags:
- name: heartbeat
  description: Heartbeat related endpoints
paths:
  /heartbeat:
    get:
      tags:
      - heartbeat
      summary: Send a heartbeat to the system
      description: Send a heartbeat to the system to report an action
      operationId: heartbeat
      responses:
        200:
          description: Successfully processed heartbeat.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'
        400:
          description: Unable to process heartbeat.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        401:
          description: User unauthorized to access the API.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-swagger-router-controller: mainController
  /saveRequest:
    post:
      summary: Lorem Ipsum
      description: Lorem Ipsum 
      operationId: saveRequest
      parameters:
      - name: usertoken
        in: header
        description: The API token issued to you in order to consume the protected
          endpoints
        required: true
        schema:
          type: string
      requestBody:
        description: Payload to save the incoming flag request
        content:
          '*/*':
            schema:
              $ref: '#/components/schemas/SaveRequest'
        required: false
      responses:
        200:
          description: Successfully saved the request into the DB.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'
        400:
          description: Failed to save the request in the DB.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        401:
          description: User unauthorized to access the API.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-swagger-router-controller: mainController
      x-codegen-request-body-name: SaveRequest
  /markActionExecuted:
    post:
      summary: Marks the action as executed for a given request.
      description: Lorem Ipsum
      operationId: markActionExecuted
      parameters:
      - name: usertoken
        in: header
        description: The API token issued to you in order to consume the protected
          endpoints
        required: true
        schema:
          type: string
      requestBody:
        description: Payload to mark action as executed.
        content:
          '*/*':
            schema:
              $ref: '#/components/schemas/MarkActionExecuted'
        required: false
      responses:
        200:
          description: Successfully marked action as executed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'
        401:
          description: User unauthorized to access the API.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        500:
          description: Error executing stored procedure to mark action executed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-swagger-router-controller: mainController
      x-codegen-request-body-name: MarkActionExecuted
  /saveAccountInfo:
    post:
      summary: Lorem Ipsum
      description: Lorem Ipsum
      operationId: saveAccountInfo
      parameters:
      - name: usertoken
        in: header
        description: The API token issued to you in order to consume the protected
          endpoints
        required: true
        schema:
          type: string
      requestBody:
        description: Payload to save the account info
        content:
          '*/*':
            schema:
              $ref: '#/components/schemas/AccountInfo'
        required: false
      responses:
        200:
          description: Successfully saved the account info in the DB.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'
        400:
          description: Failed to save the account info in the DB.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        401:
          description: User unauthorized to access the API.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-swagger-router-controller: mainController
      x-codegen-request-body-name: AccountInfo
  /forward:
    post:
      summary: Lorem Ipsum
      description: Lorem Ipsum
      operationId: forwardRequest
      parameters:
      - name: usertoken
        in: header
        description: The API token issued to you in order to consume the protected
          endpoints
        required: true
        schema:
          type: string
      requestBody:
        description: Payload used when doing forwarding checks
        content:
          '*/*':
            schema:
              $ref: '#/components/schemas/Forward'
        required: false
      responses:
        200:
          description: Successfully made forwarding decision.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'
        400:
          description: Bad request from client.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        401:
          description: User unauthorized to access the API.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-swagger-router-controller: mainController
      x-codegen-request-body-name: Forward
  /getUserAuthInfo:
    post:
      summary: Lorem Ipsum
      description: Lorem Ipsum
      operationId: getUserAuthInfo
      parameters:
      - name: usertoken
        in: header
        description: The API token issued to authenticate and authorize the protected
          endpoint
        required: true
        schema:
          type: string
      requestBody:
        description: Payload with username
        content:
          '*/*':
            schema:
              $ref: '#/components/schemas/UserAuthInfo'
        required: false
      responses:
        200:
          description: Successfully retrieved user information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'
        400:
          description: Bad request from client
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        401:
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        404:
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-swagger-router-controller: mainController
      x-codegen-request-body-name: UserAuthInfo
  /getAccountsProcessed:
    post:
      summary: Lorem Ipsum
      description: Lorem Ipsum
      operationId: getAccountsProcessed
      parameters:
      - name: usertoken
        in: header
        description: The API token issued to authenticate and authorize the protected
          endpoint
        required: true
        schema:
          type: string
      requestBody:
        description: Payload with timeframes
        content:
          '*/*':
            schema:
              $ref: '#/components/schemas/AccountsProcessedInfo'
        required: false
      responses:
        200:
          description: Successfully retrieved processed account information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'
        400:
          description: Bad request from client
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        401:
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-swagger-router-controller: mainController
      x-codegen-request-body-name: AccountsProcessedInfo
components:
  schemas:
    Success:
      type: object
      properties:
        data:
          type: object
          properties: {}
        results:
          type: object
          properties:
            err:
              type: boolean
              default: false
            msg:
              type: string
    Error:
      type: object
      properties:
        data:
          type: object
          properties: {}
        results:
          type: object
          properties:
            err:
              type: boolean
              default: true
            msg:
              type: string
    SaveRequest:
      required:
      - action
      - detectionMethod
      - detectionTime
      - identityType
      - identityValue
      - realmId
      type: object
      properties:
        identityValue:
          type: string
        identityType:
          type: string
          enum:
          - A
          - B
          - C
        realmId:
          type: string
          enum:
          - A
          - B
          - C
          - D
        action:
          type: string
          enum:
          - A
          - B
        detectionMethod:
          type: string
        zincId:
          type: string
        deferredTime:
          maximum: 2.147483647E+12
          minimum: 0
          type: integer
          format: int64
        ipAddress:
          type: string
        userAgent:
          type: string
        httpMethod:
          type: string
          enum:
          - GET
          - HEAD
          - POST
          - PUT
          - DELETE
          - CONNECT
          - OPTIONS
          - TRACE
          - PATCH
        httpUrl:
          type: string
        detectionTime:
          maximum: 2.147483647E+12
          minimum: 0
          type: integer
          format: int64
    MarkActionExecuted:
      required:
      - requestID
      type: object
      properties:
        requestID:
          maximum: 2147483647
          minimum: 1
          type: integer
          format: int32
    AccountsProcessedInfo:
      required:
      - endTime
      - startTime
      type: object
      properties:
        startTime:
          maximum: 2.147483647E+12
          minimum: 0
          type: integer
          format: int64
        endTime:
          maximum: 2.147483647E+12
          minimum: 0
          type: integer
          format: int64
    AccountInfo:
      required:
      - email
      - requestId
      - securityInfoSrc
      type: object
      properties:
        requestId:
          maximum: 2147483647
          minimum: 1
          type: integer
          format: int32
        baseInfoSrc:
          type: string
        firstName:
          type: string
        lastName:
          type: string
        omsCid:
          type: integer
        securityInfoSrc:
          type: string
        principalId:
          type: string
        loginUid:
          type: string
        realmId:
          type: string
        email:
          type: string
          format: email
          example: sample@example.com
        accountEnabled:
          type: boolean
        accountLocked:
          type: boolean
        failPasswordCount:
          maximum: 2147483647
          minimum: 0
          type: integer
          format: int32
        lastPwdUpdateTime:
          maximum: 2.147483647E+12
          minimum: 0
          type: integer
          format: int64
        creationTime:
          maximum: 2.147483647E+12
          minimum: 0
          type: integer
          format: int64
        lastLoginTime:
          maximum: 2.147483647E+12
          minimum: 0
          type: integer
          format: int64
        loginIdChangeTime:
          maximum: 2.147483647E+12
          minimum: 0
          type: integer
          format: int64
        lastActivityTime:
          maximum: 2.147483647E+12
          minimum: 0
          type: integer
          format: int64
    Forward:
      required:
      - fromHours
      - identityValue
      - realmId
      type: object
      properties:
        identityValue:
          type: string
        realmId:
          type: string
          enum:
          - A
          - B
          - C
          - D
        fromHours:
          maximum: 2147483647
          minimum: 0
          type: integer
          format: int32
        userAgent:
          type: string
        emailDomain:
          type: string
    UserAuthInfo:
      required:
      - username
      type: object
      properties:
        username:
          type: string

Here is my server.js file:

const appConfig = require("./config");
const constants = require("./constants");
const controllers = require("./controllers/mainController");
const fs = require("fs");
const path = require("path");
const app = require("express")();
const http = require("http");
const expressOpenAPI = require("express-openapi");
const swaggerExpress = require("swagger-express-mw");
const jsyaml = require("js-yaml");
const morgan = require("morgan");
const helmet = require("helmet");
const utils = require("./helpers/utils");
const logger = require("@walmart/sts-node-module-logging").getLogger(__filename);
const secureconfig = require("@walmart/sts-node-module-secureconfig");
const permissionsMiddleware = require("@walmart/sts-node-module-auth").middleware;
const errorHandlerMiddleware = require("./middleware/validationErrorHandler");
const validationMiddleware = require("./middleware/validationMiddleware");

//CORS
app.use((req, res, next) => {
    //Enabling CORS
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS,POST,PUT");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, contentType,Content-Type, Accept, Authorization");
    next();
});

// Use helmet to provide various security measures
app.use(helmet());

// Configure logging before we get too far
logger.applyUserSettings(constants.loggingOptions);

/*
 * Configure morgan for logging http requests and responses.
 * Run morgan early on in the middleware stack (before apiAuth)
 * so we can still log request/response details even if the
 * request gets denied.
 */
app.use(morgan("combined", {
    // Pipe morgan logs to our main logger
    "stream": logger.streamForHttp
}));

// Add permissions middleware, since this middlware is async, we
// use the "then" callback to insert into the app as a middleware
// when the promise is resolved.
permissionsMiddleware({
    audience: [ appConfig.env.authToken.accountService.audience ],
    issuer: appConfig.env.authToken.issuer,
    publicKey: appConfig.env.authToken.keys.public,
    bypassAuth: [
        "^\/info\/api\/heartbeat$",
        "^\/docs.*"
    ],
    authorize: utils.authorize
}).then((mw) => app.use(mw));

// Figure out where our controllers are. They could be in one of two locations
let controllerDir = path.join(__dirname, "controllers");
controllerDir = fs.existsSync(controllerDir) ? controllerDir : path.join(path.resolve(__dirname, ".."), "src/controllers");

// The Swagger document (require it, build it programmatically, fetch it)
const spec = fs.readFileSync(path.join(__dirname, "api/swagger/swagger.yaml"), "utf8");
const swaggerDoc = jsyaml.safeLoad(spec);

function validateAllResponses(req, res, next) {
    const strictValidation = req.apiDoc["x-express-openapi-validation-strict"] ? true : false;
    if (typeof res.validateResponse === "function") {
        const send = res.send;
        res.send = function expressOpenAPISend(...args) {
          const onlyWarn = !strictValidation;
          if (res.get("x-express-openapi-validation-error-for") !== undefined) {
              return send.apply(res, args);
          }
          const body = args[0];
          let validation = res.validateResponse(res.statusCode, body);
          let validationMessage;
          if (validation === undefined) {
              validation = { message: undefined, errors: undefined };
          }
          if (validation.errors) {
              const errorList = Array.from(validation.errors).map(_ => _.message).join(",");
              validationMessage = `Invalid response for status code ${res.statusCode}: ${errorList}`;
              console.warn(validationMessage);
              // Set to avoid a loop, and to provide the original status code
              res.set("x-express-openapi-validation-error-for", res.statusCode.toString());
          }
          if (onlyWarn || !validation.errors) {
              return send.apply(res, args);
          } else {
              res.status(500);
              return res.json({ error: validationMessage });
          }
      };
    }
    next();
}

expressOpenAPI.initialize({
    app,
    apiDoc: { 
        ...swaggerDoc,
        "x-express-openapi-additional-middleware": [ validateAllResponses ],
        "x-express-openapi-validation-strict": true
    },
    operations: {
        heartbeat: controllers.heartbeat,
        saveRequest: controllers.saveRequest,
        markActionExecuted: controllers.markActionExecuted,
        saveAccountInfo: controllers.saveAccountInfo,
        forwardRequest: controllers.forwardRequest,
        getUserAuthInfo: controllers.getUserAuthInfo,
        getAccountsProcessed: controllers.getAccountsProcessed
    },
    paths: controllerDir
});

// Handle any Swagger validation failures
app.use(errorHandlerMiddleware.errorHandler);

// Add validation for identity type and realm
app.use(validationMiddleware.validateIdentityAndRealm);

const swaggerExpressConfig = {
    appRoot: __dirname
};

// Set up swagger express middleware (needed for swagger router to do its magic)
swaggerExpress.create(swaggerExpressConfig, (err, swaggerExpressApp) => {
    if (err) {
        throw err;
    }
    swaggerExpressApp.register(app);
});

// Start the server only after the secureconfig initialization is done and all the
// environment secrets are loaded.
secureconfig.initConfig().then(() => {
    logger.info(`Initialized the secureconfig module`);
    appConfig.loadSecrets();

    // Setting up server
    const server = http.createServer(app);
    const port = process.env.PORT || appConfig.env.port;
    server.listen(port);
    logger.info(`Server started on port: ${port}; Mode: ${appConfig.envMode}`);
}).catch((err) => {
    logger.fatal("Failed to start server", err);
});

Thanks!

noaoh commented 3 years ago

I removed swagger-express-mw from server.js and that fixed the issue.