[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
This commit is contained in:
Elias Meire 2019-11-01 15:30:44 +01:00 committed by Gregory Schier
parent 9b804b282b
commit 414e35ea11
9 changed files with 509 additions and 14 deletions

View File

@ -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": [],

View File

@ -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": [],

View File

@ -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

View File

@ -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"
}
]
}

View File

@ -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

View File

@ -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"
}
]
}

View File

@ -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": [],

View File

@ -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": [],

View File

@ -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<Object|null>} 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)
*