From 414e35ea112e4df3bf47b0f8f7bc3229330cd2bd Mon Sep 17 00:00:00 2001 From: Elias Meire <8850410+eliasmeire@users.noreply.github.com> Date: Fri, 1 Nov 2019 15:30:44 +0100 Subject: [PATCH] [Feature] [OpenAPI] Support OpenAPI 3 securitySchemes (#1725) * feat(openapi): Fix parseDocument return type * feat(openapi): Add support for apiKey and http security Add support for apiKey and http security in OpenAPI 3 parser * feat(openapi): Add env variables for OpenAPI security * feat(openapi): Restore fixture tests, add skip option for failing test * feat(openapi): Add/adapt test fixtures * feat(openapi): Improve Cookie formatting * Empty commit; trigger CI * feat(openapi): Add type basic to auth --- .../openapi3/dereferenced-output.json | 17 ++- .../dereferenced-with-tags-output.json | 17 ++- .../openapi3/endpoint-security-input.yaml | 64 ++++++++ .../openapi3/endpoint-security-output.json | 141 ++++++++++++++++++ .../openapi3/global-security-input.yaml | 42 ++++++ .../openapi3/global-security-output.json | 78 ++++++++++ .../fixtures/openapi3/petstore-output.json | 17 ++- .../openapi3/petstore-with-tags-output.json | 17 ++- .../src/importers/openapi3.js | 130 +++++++++++++++- 9 files changed, 509 insertions(+), 14 deletions(-) create mode 100644 packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-input.yaml create mode 100644 packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-output.json create mode 100644 packages/insomnia-importers/src/__tests__/fixtures/openapi3/global-security-input.yaml create mode 100644 packages/insomnia-importers/src/__tests__/fixtures/openapi3/global-security-output.json diff --git a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/dereferenced-output.json b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/dereferenced-output.json index 06d655e60..8c6dabac4 100644 --- a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/dereferenced-output.json +++ b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/dereferenced-output.json @@ -24,6 +24,7 @@ "_id": "env___BASE_ENVIRONMENT_ID___sub", "_type": "environment", "data": { + "apiKey": "apiKey", "base_path": "/v2", "host": "petstore.swagger.io", "scheme": "http" @@ -102,7 +103,13 @@ "_type": "request", "authentication": {}, "body": {}, - "headers": [], + "headers": [ + { + "disabled": false, + "name": "api_key", + "value": "{{ apiKey }}" + } + ], "method": "GET", "name": "Find pet by ID", "parameters": [], @@ -160,7 +167,13 @@ "_type": "request", "authentication": {}, "body": {}, - "headers": [], + "headers": [ + { + "disabled": false, + "name": "api_key", + "value": "{{ apiKey }}" + } + ], "method": "GET", "name": "Returns pet inventories by status", "parameters": [], diff --git a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/dereferenced-with-tags-output.json b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/dereferenced-with-tags-output.json index 2d34d042b..cb35525a9 100644 --- a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/dereferenced-with-tags-output.json +++ b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/dereferenced-with-tags-output.json @@ -24,6 +24,7 @@ "_id": "env___BASE_ENVIRONMENT_ID___sub", "_type": "environment", "data": { + "apiKey": "apiKey", "base_path": "/v2", "host": "petstore.swagger.io", "scheme": "http" @@ -126,7 +127,13 @@ "_type": "request", "authentication": {}, "body": {}, - "headers": [], + "headers": [ + { + "disabled": false, + "name": "api_key", + "value": "{{ apiKey }}" + } + ], "method": "GET", "name": "Find pet by ID", "parameters": [], @@ -184,7 +191,13 @@ "_type": "request", "authentication": {}, "body": {}, - "headers": [], + "headers": [ + { + "disabled": false, + "name": "api_key", + "value": "{{ apiKey }}" + } + ], "method": "GET", "name": "Returns pet inventories by status", "parameters": [], diff --git a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-input.yaml b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-input.yaml new file mode 100644 index 000000000..c12c81463 --- /dev/null +++ b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-input.yaml @@ -0,0 +1,64 @@ +openapi: '3.0.2' +info: + title: Endpoint Security + version: '1.2' +servers: + - url: https://api.server.test/v1 + +components: + securitySchemes: + Basic: + type: http + scheme: basic + Key-Header: + type: apiKey + name: x-api_key + in: header + Key-Cookie: + type: apiKey + name: CookieName + in: cookie + Key-Query: + type: apiKey + name: key + in: query + +paths: + /basic: + get: + security: + - Basic: [] + responses: + '200': + description: OK + /key/header: + get: + security: + - Key-Header: [] + responses: + '200': + description: OK + /key/cookie: + get: + security: + - Key-Cookie: [] + responses: + '200': + description: OK + /key/query: + get: + security: + - Key-Query: [] + responses: + '200': + description: OK + /all: + get: + security: + - Basic: [] + - Key-Query: [] + - Key-Header: [] + - Key-Cookie: [] + responses: + '200': + description: OK diff --git a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-output.json b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-output.json new file mode 100644 index 000000000..483901920 --- /dev/null +++ b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/endpoint-security-output.json @@ -0,0 +1,141 @@ +{ + "__export_date": "2019-10-12T17:07:14.568Z", + "__export_format": 4, + "__export_source": "insomnia.importers:v0.1.0", + "_type": "export", + "resources": [ + { + "_id": "__WORKSPACE_ID__", + "_type": "workspace", + "description": "", + "name": "Endpoint Security 1.2", + "parentId": null + }, + { + "_id": "__BASE_ENVIRONMENT_ID__", + "_type": "environment", + "data": { + "base_url": "{{ scheme }}://{{ host }}{{ base_path }}" + }, + "name": "Base environment", + "parentId": "__WORKSPACE_ID__" + }, + { + "_id": "env___BASE_ENVIRONMENT_ID___sub", + "_type": "environment", + "data": { + "apiKey": "apiKey", + "base_path": "/v1", + "host": "api.server.test", + "httpPassword": "password", + "httpUsername": "username", + "scheme": "https" + }, + "name": "OpenAPI env", + "parentId": "__BASE_ENVIRONMENT_ID__" + }, + { + "_id": "req___WORKSPACE_ID__4a563129", + "_type": "request", + "authentication": { + "password": "{{ httpPassword }}", + "type": "basic", + "username": "{{ httpUsername }}" + }, + "body": {}, + "headers": [], + "method": "GET", + "name": "/basic", + "parameters": [], + "parentId": "__WORKSPACE_ID__", + "url": "{{ base_url }}/basic" + }, + { + "_id": "req___WORKSPACE_ID__48bba8a5", + "_type": "request", + "authentication": {}, + "body": {}, + "headers": [ + { + "disabled": false, + "name": "x-api_key", + "value": "{{ apiKey }}" + } + ], + "method": "GET", + "name": "/key/header", + "parameters": [], + "parentId": "__WORKSPACE_ID__", + "url": "{{ base_url }}/key/header" + }, + { + "_id": "req___WORKSPACE_ID__2ea006cf", + "_type": "request", + "authentication": {}, + "body": {}, + "headers": [ + { + "disabled": false, + "name": "Cookie", + "value": "CookieName={{ apiKey }}" + } + ], + "method": "GET", + "name": "/key/cookie", + "parameters": [], + "parentId": "__WORKSPACE_ID__", + "url": "{{ base_url }}/key/cookie" + }, + { + "_id": "req___WORKSPACE_ID__0a8d5285", + "_type": "request", + "authentication": {}, + "body": {}, + "headers": [], + "method": "GET", + "name": "/key/query", + "parameters": [ + { + "disabled": false, + "name": "key", + "value": "{{ apiKey }}" + } + ], + "parentId": "__WORKSPACE_ID__", + "url": "{{ base_url }}/key/query" + }, + { + "_id": "req___WORKSPACE_ID__e285189c", + "_type": "request", + "authentication": { + "password": "{{ httpPassword }}", + "type": "basic", + "username": "{{ httpUsername }}" + }, + "body": {}, + "headers": [ + { + "disabled": false, + "name": "x-api_key", + "value": "{{ apiKey }}" + }, + { + "disabled": false, + "name": "Cookie", + "value": "CookieName={{ apiKey }}" + } + ], + "method": "GET", + "name": "/all", + "parameters": [ + { + "disabled": false, + "name": "key", + "value": "{{ apiKey }}" + } + ], + "parentId": "__WORKSPACE_ID__", + "url": "{{ base_url }}/all" + } + ] +} diff --git a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/global-security-input.yaml b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/global-security-input.yaml new file mode 100644 index 000000000..d690f5b7b --- /dev/null +++ b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/global-security-input.yaml @@ -0,0 +1,42 @@ +openapi: '3.0.2' +info: + title: Global Security + version: '1.2' +servers: + - url: https://api.server.test/v1 + +components: + securitySchemes: + Basic: + type: http + scheme: basic + Key-Header: + type: apiKey + name: x-api_key + in: header + Key-Cookie: + type: apiKey + name: CookieName + in: cookie + Key-Query: + type: apiKey + name: apiKeyHere + in: query + +security: + - Basic: [] + - Key-Header: [] + +paths: + /global: + get: + responses: + '200': + description: OK + /override: + get: + security: + - Key-Query: [] + responses: + '200': + description: OK diff --git a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/global-security-output.json b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/global-security-output.json new file mode 100644 index 000000000..331325d76 --- /dev/null +++ b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/global-security-output.json @@ -0,0 +1,78 @@ +{ + "__export_date": "2019-10-12T17:23:25.171Z", + "__export_format": 4, + "__export_source": "insomnia.importers:v0.1.0", + "_type": "export", + "resources": [ + { + "_id": "__WORKSPACE_ID__", + "_type": "workspace", + "description": "", + "name": "Global Security 1.2", + "parentId": null + }, + { + "_id": "__BASE_ENVIRONMENT_ID__", + "_type": "environment", + "data": { + "base_url": "{{ scheme }}://{{ host }}{{ base_path }}" + }, + "name": "Base environment", + "parentId": "__WORKSPACE_ID__" + }, + { + "_id": "env___BASE_ENVIRONMENT_ID___sub", + "_type": "environment", + "data": { + "apiKey": "apiKey", + "base_path": "/v1", + "host": "api.server.test", + "httpPassword": "password", + "httpUsername": "username", + "scheme": "https" + }, + "name": "OpenAPI env", + "parentId": "__BASE_ENVIRONMENT_ID__" + }, + { + "_id": "req___WORKSPACE_ID__21946b60", + "_type": "request", + "authentication": { + "password": "{{ httpPassword }}", + "type": "basic", + "username": "{{ httpUsername }}" + }, + "body": {}, + "headers": [ + { + "disabled": false, + "name": "x-api_key", + "value": "{{ apiKey }}" + } + ], + "method": "GET", + "name": "/global", + "parameters": [], + "parentId": "__WORKSPACE_ID__", + "url": "{{ base_url }}/global" + }, + { + "_id": "req___WORKSPACE_ID__b410454b", + "_type": "request", + "authentication": {}, + "body": {}, + "headers": [], + "method": "GET", + "name": "/override", + "parameters": [ + { + "disabled": false, + "name": "apiKeyHere", + "value": "{{ apiKey }}" + } + ], + "parentId": "__WORKSPACE_ID__", + "url": "{{ base_url }}/override" + } + ] +} diff --git a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/petstore-output.json b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/petstore-output.json index 346438678..e91dff9e2 100644 --- a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/petstore-output.json +++ b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/petstore-output.json @@ -24,6 +24,7 @@ "_id": "env___BASE_ENVIRONMENT_ID___sub", "_type": "environment", "data": { + "apiKey": "apiKey", "base_path": "/v2", "host": "petstore.swagger.io", "scheme": "http" @@ -102,7 +103,13 @@ "_type": "request", "authentication": {}, "body": {}, - "headers": [], + "headers": [ + { + "disabled": false, + "name": "api_key", + "value": "{{ apiKey }}" + } + ], "method": "GET", "name": "Find pet by ID", "parameters": [], @@ -160,7 +167,13 @@ "_type": "request", "authentication": {}, "body": {}, - "headers": [], + "headers": [ + { + "disabled": false, + "name": "api_key", + "value": "{{ apiKey }}" + } + ], "method": "GET", "name": "Returns pet inventories by status", "parameters": [], diff --git a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/petstore-with-tags-output.json b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/petstore-with-tags-output.json index 869c61dad..8e7d6e549 100644 --- a/packages/insomnia-importers/src/__tests__/fixtures/openapi3/petstore-with-tags-output.json +++ b/packages/insomnia-importers/src/__tests__/fixtures/openapi3/petstore-with-tags-output.json @@ -24,6 +24,7 @@ "_id": "env___BASE_ENVIRONMENT_ID___sub", "_type": "environment", "data": { + "apiKey": "apiKey", "base_path": "/v2", "host": "petstore.swagger.io", "scheme": "http" @@ -126,7 +127,13 @@ "_type": "request", "authentication": {}, "body": {}, - "headers": [], + "headers": [ + { + "disabled": false, + "name": "api_key", + "value": "{{ apiKey }}" + } + ], "method": "GET", "name": "Find pet by ID", "parameters": [], @@ -184,7 +191,13 @@ "_type": "request", "authentication": {}, "body": {}, - "headers": [], + "headers": [ + { + "disabled": false, + "name": "api_key", + "value": "{{ apiKey }}" + } + ], "method": "GET", "name": "Returns pet inventories by status", "parameters": [], diff --git a/packages/insomnia-importers/src/importers/openapi3.js b/packages/insomnia-importers/src/importers/openapi3.js index ad8d38027..ab2932577 100644 --- a/packages/insomnia-importers/src/importers/openapi3.js +++ b/packages/insomnia-importers/src/importers/openapi3.js @@ -11,6 +11,13 @@ const MIMETYPE_JSON = 'application/json'; const MIMETYPE_LITERALLY_ANYTHING = '*/*'; const SUPPORTED_MIME_TYPES = [MIMETYPE_JSON, MIMETYPE_LITERALLY_ANYTHING]; const WORKSPACE_ID = '__WORKSPACE_ID__'; +const SECURITY_TYPE = { + HTTP: 'http', + API_KEY: 'apiKey', + OAUTH: 'oauth2', + OPEN_ID: 'openIdConnect', +}; +const SUPPORTED_SECURITY_TYPES = [SECURITY_TYPE.HTTP, SECURITY_TYPE.API_KEY]; let requestCounts = {}; @@ -55,6 +62,9 @@ module.exports.convert = async function(rawData) { const servers = api.servers.map(s => new URL(s.url)); const defaultServer = servers[0] || new URL('http://example.com/'); + const securityVariables = getSecurityEnvVariables( + api.components && api.components.securitySchemes, + ); const openapiEnv = { _type: 'environment', @@ -65,6 +75,7 @@ module.exports.convert = async function(rawData) { base_path: defaultServer.pathname || '', scheme: defaultServer.protocol.replace(/:$/, '') || ['http'], // note: `URL.protocol` returns with trailing `:` (i.e. "https:") host: defaultServer.host || '', + ...securityVariables, }, }; @@ -78,7 +89,7 @@ module.exports.convert = async function(rawData) { * * @param {string} rawData * - * @returns {Object|null} OpenAPI 3 object + * @returns {Promise} OpenAPI 3 object */ async function parseDocument(rawData) { try { @@ -96,6 +107,8 @@ async function parseDocument(rawData) { * @returns {Object[]} array of insomnia endpoints definitions */ function parseEndpoints(document) { + const rootSecurity = document.security; + const securitySchemes = document.components ? document.components.securitySchemes : {}; const defaultParent = WORKSPACE_ID; const paths = Object.keys(document.paths); @@ -131,7 +144,8 @@ function parseEndpoints(document) { for (const tag of tags) { const parentId = folderLookup[tag] || defaultParent; - requests.push(importRequest(endpointSchema, parentId)); + const resolvedSecurity = endpointSchema.security || rootSecurity; + requests.push(importRequest(endpointSchema, parentId, resolvedSecurity, securitySchemes)); } }); @@ -167,13 +181,20 @@ function importFolderItem(item, parentId) { * * @param {Object} endpointSchema - OpenAPI 3 endpoint schema * @param {string} parentId - id of parent category + * @param {Object} security - OpenAPI 3 security rules + * @param {Object} securitySchemes - OpenAPI 3 security schemes * @returns {Object} */ -function importRequest(endpointSchema, parentId) { +function importRequest(endpointSchema, parentId, security, securitySchemes) { const name = endpointSchema.summary || endpointSchema.path; const id = generateUniqueRequestId(endpointSchema); + const paramHeaders = prepareHeaders(endpointSchema); + const { authentication, headers: securityHeaders, parameters: securityParams } = parseSecurity( + security, + securitySchemes, + ); - return { + const request = { _type: 'request', _id: id, parentId: parentId, @@ -181,9 +202,12 @@ function importRequest(endpointSchema, parentId) { method: endpointSchema.method.toUpperCase(), url: '{{ base_url }}' + pathWithParamsAsVariables(endpointSchema.path), body: prepareBody(endpointSchema), - headers: prepareHeaders(endpointSchema), - parameters: prepareQueryParams(endpointSchema), + headers: [...paramHeaders, ...securityHeaders], + authentication, + parameters: [...prepareQueryParams(endpointSchema), ...securityParams], }; + + return request; } /** @@ -224,6 +248,100 @@ function prepareHeaders(endpointSchema) { return convertParameters(headerParameters); } +/** + * Parse OpenAPI 3 securitySchemes into insomnia definitions of authentication, headers and parameters + * + * @param {Object} security - OpenAPI 3 security rules + * @param {Object} securitySchemes - OpenAPI 3 security schemes + * @returns {Object} headers or basic http authentication details + */ +function parseSecurity(security, securitySchemes) { + if (!security || !securitySchemes) { + return { + authentication: {}, + headers: [], + parameters: [], + }; + } + + const supportedSchemes = security + .map(securityPolicy => { + const securityName = Object.keys(securityPolicy)[0]; + return securitySchemes[securityName]; + }) + .filter(schemeDetails => SUPPORTED_SECURITY_TYPES.includes(schemeDetails.type)); + + const apiKeySchemes = supportedSchemes.filter(scheme => scheme.type === SECURITY_TYPE.API_KEY); + const apiKeyHeaders = apiKeySchemes + .filter(scheme => scheme.in === 'header') + .map(scheme => { + return { + name: scheme.name, + disabled: false, + value: '{{ apiKey }}', + }; + }); + const apiKeyCookies = apiKeySchemes + .filter(scheme => scheme.in === 'cookie') + .map(scheme => `${scheme.name}={{ apiKey }}`); + const apiKeyCookieHeader = { name: 'Cookie', disabled: false, value: apiKeyCookies.join('; ') }; + const apiKeyParams = apiKeySchemes + .filter(scheme => scheme.in === 'query') + .map(scheme => { + return { + name: scheme.name, + disabled: false, + value: '{{ apiKey }}', + }; + }); + + if (apiKeyCookies.length > 0) { + apiKeyHeaders.push(apiKeyCookieHeader); + } + + const httpAuth = supportedSchemes.find( + scheme => scheme.type === SECURITY_TYPE.HTTP && scheme.scheme === 'basic', + ) + ? { type: 'basic', username: '{{ httpUsername }}', password: '{{ httpPassword }}' } + : {}; + + return { + authentication: httpAuth, + headers: apiKeyHeaders, + parameters: apiKeyParams, + }; +} + +/** + * Get Insomnia environment variables for OpenAPI securitySchemes + * + * @param {Object} securitySchemes - Open API security schemes + * @returns {Object} Insomnia environment variables containing security information + */ +function getSecurityEnvVariables(securitySchemes) { + if (!securitySchemes) { + return {}; + } + + const variables = {}; + const securitySchemesArray = Object.values(securitySchemes); + const hasApiKeyScheme = securitySchemesArray.some( + scheme => scheme.type === SECURITY_TYPE.API_KEY, + ); + const hasHttpScheme = securitySchemesArray.some(scheme => scheme.type === SECURITY_TYPE.HTTP); + + if (hasApiKeyScheme) { + variables.apiKey = 'apiKey'; + } + + if (hasHttpScheme) { + variables.httpUsername = 'username'; + variables.httpPassword = 'password'; + } + + return variables; +} + /** * Imports insomnia request body definitions, including data mock (if available) *