From c6eb8edb5f42c40122247c4593754fc3f0218131 Mon Sep 17 00:00:00 2001 From: kyle Date: Thu, 1 Nov 2018 18:52:13 -0400 Subject: [PATCH] feature: Docker OAuth block support (via #4987) * add `onFound` callback to schemas * add warning to method docs (for #4957) * implement Docker OAuth2 init block support * update docs * add OAUTH_SCOPE_SEPARATOR * drop OAuth env from Dockerfile and run script * don't indent the first oauth block line * drop unused `dedent` import * touch up warning message * add more test cases * return an empty block if no OAuth content is generated * fix broken doc line --- Dockerfile | 5 - docker/configurator/helpers.js | 13 ++ docker/configurator/index.js | 21 +-- docker/configurator/oauth.js | 43 +++++ docker/configurator/translator.js | 4 + docker/run.sh | 7 - docs/usage/configuration.md | 6 +- docs/usage/oauth2.md | 18 +- test/docker/oauth.js | 58 +++++++ test/docker/translator.js | 274 ++++++++++++++++-------------- 10 files changed, 286 insertions(+), 163 deletions(-) create mode 100644 docker/configurator/helpers.js create mode 100644 docker/configurator/oauth.js create mode 100644 test/docker/oauth.js diff --git a/Dockerfile b/Dockerfile index 03381839..1d71c0ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,11 +9,6 @@ RUN apk add nodejs LABEL maintainer="fehguy" ENV API_KEY "**None**" -ENV OAUTH_CLIENT_ID "**None**" -ENV OAUTH_CLIENT_SECRET "**None**" -ENV OAUTH_REALM "**None**" -ENV OAUTH_APP_NAME "**None**" -ENV OAUTH_ADDITIONAL_PARAMS "**None**" ENV SWAGGER_JSON "/app/swagger.json" ENV PORT 8080 ENV BASE_URL "" diff --git a/docker/configurator/helpers.js b/docker/configurator/helpers.js new file mode 100644 index 00000000..c72479d6 --- /dev/null +++ b/docker/configurator/helpers.js @@ -0,0 +1,13 @@ +module.exports.indent = function indent(str, len, fromLine = 0) { + + return str + .split("\n") + .map((line, i) => { + if (i + 1 >= fromLine) { + return `${Array(len + 1).join(" ")}${line}` + } else { + return line + } + }) + .join("\n") +} \ No newline at end of file diff --git a/docker/configurator/index.js b/docker/configurator/index.js index 38567fcd..1144d03b 100755 --- a/docker/configurator/index.js +++ b/docker/configurator/index.js @@ -2,7 +2,8 @@ const fs = require("fs") const path = require("path") const translator = require("./translator") -const configSchema = require("./variables") +const oauthBlockBuilder = require("./oauth") +const indent = require("./helpers").indent const START_MARKER = "// Begin Swagger UI call region" const END_MARKER = "// End Swagger UI call region" @@ -22,19 +23,7 @@ fs.writeFileSync(targetPath, `${beforeStartMarkerContent} const ui = SwaggerUIBundle({ ${indent(translator(process.env, { injectBaseConfig: true }), 8, 2)} }) + + ${indent(oauthBlockBuilder(process.env), 6, 2)} ${END_MARKER} -${afterEndMarkerContent}`) - -function indent(str, len, fromLine) { - - return str - .split("\n") - .map((line, i) => { - if(i + 1 >= fromLine) { - return `${Array(len + 1).join(" ")}${line}` - } else { - return line - } - }) - .join("\n") -} \ No newline at end of file +${afterEndMarkerContent}`) \ No newline at end of file diff --git a/docker/configurator/oauth.js b/docker/configurator/oauth.js new file mode 100644 index 00000000..93fed5ba --- /dev/null +++ b/docker/configurator/oauth.js @@ -0,0 +1,43 @@ +const translator = require("./translator") +const indent = require("./helpers").indent + +const oauthBlockSchema = { + OAUTH_CLIENT_ID: { + type: "string", + name: "clientId" + }, + OAUTH_CLIENT_SECRET: { + type: "string", + name: "clientSecret", + onFound: () => console.warn("Swagger UI warning: don't use `OAUTH_CLIENT_SECRET` in production!") + }, + OAUTH_REALM: { + type: "string", + name: "realm" + }, + OAUTH_APP_NAME: { + type: "string", + name: "appName" + }, + OAUTH_SCOPE_SEPARATOR: { + type: "string", + name: "scopeSeparator" + }, + OAUTH_ADDITIONAL_PARAMS: { + type: "object", + name: "additionalQueryStringParams" + } +} + +module.exports = function oauthBlockBuilder(env) { + const translatorResult = translator(env, { schema: oauthBlockSchema }) + + if(translatorResult) { + return ( + `ui.initOAuth({ +${indent(translatorResult, 2)} +})`) + } + + return `` +} \ No newline at end of file diff --git a/docker/configurator/translator.js b/docker/configurator/translator.js index 06167553..1671672e 100644 --- a/docker/configurator/translator.js +++ b/docker/configurator/translator.js @@ -55,6 +55,10 @@ function objectToKeyValueString(env, { injectBaseConfig = false, schema = config if(!varSchema) return + if(varSchema.onFound) { + varSchema.onFound() + } + const storageContents = valueStorage[varSchema.name] if(storageContents) { diff --git a/docker/run.sh b/docker/run.sh index 7ea77b3e..e5551315 100644 --- a/docker/run.sh +++ b/docker/run.sh @@ -28,13 +28,6 @@ if [ "${BASE_URL}" ]; then fi replace_in_index myApiKeyXXXX123456789 $API_KEY -replace_or_delete_in_index your-client-id $OAUTH_CLIENT_ID -replace_or_delete_in_index your-client-secret-if-required $OAUTH_CLIENT_SECRET -replace_or_delete_in_index your-realms $OAUTH_REALM -replace_or_delete_in_index your-app-name $OAUTH_APP_NAME -if [ "$OAUTH_ADDITIONAL_PARAMS" != "**None**" ]; then - replace_in_index "additionalQueryStringParams: {}" "additionalQueryStringParams: {$OAUTH_ADDITIONAL_PARAMS}" -fi if [[ -f $SWAGGER_JSON ]]; then cp -s $SWAGGER_JSON $NGINX_ROOT diff --git a/docs/usage/configuration.md b/docs/usage/configuration.md index 8345d80a..cea3f2cd 100644 --- a/docs/usage/configuration.md +++ b/docs/usage/configuration.md @@ -81,9 +81,11 @@ Parameter name | Docker variable | Description ### Instance methods -Parameter name | Docker variable | Description +**💡 Take note! These are methods, not parameters**. + +Method name | Docker variable | Description --- | --- | ----- -`initOAuth` | _Unavailable_ | `(configObj) => void`. Provide Swagger-UI with information about your OAuth server - see the OAuth2 documentation for more information. +`initOAuth` | [_See `oauth2.md`_](./oauth2.md) | `(configObj) => void`. Provide Swagger-UI with information about your OAuth server - see the OAuth2 documentation for more information. `preauthorizeBasic` | _Unavailable_ | `(authDefinitionKey, username, password) => action`. Programmatically set values for a Basic authorization scheme. `preauthorizeApiKey` | _Unavailable_ | `(authDefinitionKey, apiKeyValue) => action`. Programmatically set values for an API key authorization scheme. diff --git a/docs/usage/oauth2.md b/docs/usage/oauth2.md index 3289ae95..4918374c 100644 --- a/docs/usage/oauth2.md +++ b/docs/usage/oauth2.md @@ -1,15 +1,15 @@ # OAuth2 configuration You can configure OAuth2 authorization by calling the `initOAuth` method. -Config Name | Description ---- | --- -clientId | Default clientId. MUST be a string -clientSecret | **🚨 Never use this parameter in your production environemnt. It exposes cruicial security information. This feature is intended for dev/test environments only. 🚨**
Default clientSecret. MUST be a string -realm | realm query parameter (for oauth1) added to `authorizationUrl` and `tokenUrl`. MUST be a string -appName | application name, displayed in authorization popup. MUST be a string -scopeSeparator | scope separator for passing scopes, encoded before calling, default value is a space (encoded value `%20`). MUST be a string -additionalQueryStringParams | Additional query parameters added to `authorizationUrl` and `tokenUrl`. MUST be an object -useBasicAuthenticationWithAccessCodeGrant | Only activated for the `accessCode` flow. During the `authorization_code` request to the `tokenUrl`, pass the [Client Password](https://tools.ietf.org/html/rfc6749#section-2.3.1) using the HTTP Basic Authentication scheme (`Authorization` header with `Basic base64encode(client_id + client_secret)`). The default is `false` +Property name | Docker variable | Description +--- | --- | ------ +clientId | `OAUTH_CLIENT_ID` | Default clientId. MUST be a string +clientSecret | `OAUTH_CLIENT_SECRET` | **🚨 Never use this parameter in your production environemnt. It exposes cruicial security information. This feature is intended for dev/test environments only. 🚨**
Default clientSecret. MUST be a string +realm | `OAUTH_REALM` |realm query parameter (for oauth1) added to `authorizationUrl` and `tokenUrl`. MUST be a string +appName | `OAUTH_APP_NAME` |application name, displayed in authorization popup. MUST be a string +scopeSeparator | `OAUTH_SCOPE_SEPARATOR` |scope separator for passing scopes, encoded before calling, default value is a space (encoded value `%20`). MUST be a string +additionalQueryStringParams | `OAUTH_ADDITIONAL_PARAMS` |Additional query parameters added to `authorizationUrl` and `tokenUrl`. MUST be an object +useBasicAuthenticationWithAccessCodeGrant | _Unavailable_ |Only activated for the `accessCode` flow. During the `authorization_code` request to the `tokenUrl`, pass the [Client Password](https://tools.ietf.org/html/rfc6749#section-2.3.1) using the HTTP Basic Authentication scheme (`Authorization` header with `Basic base64encode(client_id + client_secret)`). The default is `false` ```javascript const ui = SwaggerUI({...}) diff --git a/test/docker/oauth.js b/test/docker/oauth.js new file mode 100644 index 00000000..3a6ffc3d --- /dev/null +++ b/test/docker/oauth.js @@ -0,0 +1,58 @@ +const expect = require("expect") +const oauthBlockBuilder = require("../../docker/configurator/oauth") +const dedent = require("dedent") + +describe("docker: env translator - oauth block", function() { + it("should omit the block if there are no valid keys", function () { + const input = {} + + expect(oauthBlockBuilder(input)).toEqual(``) + }) + it("should omit the block if there are no valid keys", function () { + const input = { + NOT_A_VALID_KEY: "asdf1234" + } + + expect(oauthBlockBuilder(input)).toEqual(``) + }) + it("should generate a block from empty values", function() { + const input = { + OAUTH_CLIENT_ID: ``, + OAUTH_CLIENT_SECRET: ``, + OAUTH_REALM: ``, + OAUTH_APP_NAME: ``, + OAUTH_SCOPE_SEPARATOR: "", + OAUTH_ADDITIONAL_PARAMS: ``, + } + + expect(oauthBlockBuilder(input)).toEqual(dedent(` + ui.initOAuth({ + clientId: "", + clientSecret: "", + realm: "", + appName: "", + scopeSeparator: "", + additionalQueryStringParams: undefined, + })`)) + }) + it("should generate a full block", function() { + const input = { + OAUTH_CLIENT_ID: `myId`, + OAUTH_CLIENT_SECRET: `mySecret`, + OAUTH_REALM: `myRealm`, + OAUTH_APP_NAME: `myAppName`, + OAUTH_SCOPE_SEPARATOR: "%21", + OAUTH_ADDITIONAL_PARAMS: `{ "a": 1234, "b": "stuff" }`, + } + + expect(oauthBlockBuilder(input)).toEqual(dedent(` + ui.initOAuth({ + clientId: "myId", + clientSecret: "mySecret", + realm: "myRealm", + appName: "myAppName", + scopeSeparator: "%21", + additionalQueryStringParams: { "a": 1234, "b": "stuff" }, + })`)) + }) +}) \ No newline at end of file diff --git a/test/docker/translator.js b/test/docker/translator.js index 887c8fc1..bf7597e7 100644 --- a/test/docker/translator.js +++ b/test/docker/translator.js @@ -3,18 +3,43 @@ const translator = require("../../docker/configurator/translator") const dedent = require("dedent") describe("docker: env translator", function() { - it("should generate an empty baseline config", function() { - const input = {} + describe("fundamentals", function() { + it("should generate an empty baseline config", function () { + const input = {} - expect(translator(input)).toEqual(``) - }) + expect(translator(input)).toEqual(``) + }) + + it("should call an onFound callback", function () { + const input = { + MY_THING: "hey" + } + + const onFoundSpy = expect.createSpy() + + const schema = { + MY_THING: { + type: "string", + name: "myThing", + onFound: onFoundSpy + } + } + + const res = translator(input, { + schema + }) + expect(res).toEqual(`myThing: "hey",`) + expect(onFoundSpy.calls.length).toEqual(1) - it("should generate a base config including the base content", function() { - const input = {} + }) + }) + describe("Swagger UI configuration", function() { + it("should generate a base config including the base content", function () { + const input = {} - expect(translator(input, { - injectBaseConfig: true - })).toEqual(dedent(` + expect(translator(input, { + injectBaseConfig: true + })).toEqual(dedent(` url: "https://petstore.swagger.io/v2/swagger.json", "dom_id": "#swagger-ui", deepLinking: true, @@ -27,121 +52,121 @@ describe("docker: env translator", function() { ], layout: "StandaloneLayout", `)) - }) + }) - it("should ignore an unknown config", function() { - const input = { - ASDF1234: "wow hello" - } + it("should ignore an unknown config", function () { + const input = { + ASDF1234: "wow hello" + } - expect(translator(input)).toEqual(dedent(``)) - }) + expect(translator(input)).toEqual(dedent(``)) + }) - it("should generate a string config", function() { - const input = { - URL: "http://petstore.swagger.io/v2/swagger.json", - FILTER: "" - } + it("should generate a string config", function () { + const input = { + URL: "http://petstore.swagger.io/v2/swagger.json", + FILTER: "" + } - expect(translator(input)).toEqual(dedent(` + expect(translator(input)).toEqual(dedent(` url: "http://petstore.swagger.io/v2/swagger.json", filter: "",` - ).trim()) - }) + ).trim()) + }) - it("should generate a boolean config", function() { - const input = { - DEEP_LINKING: "true", - SHOW_EXTENSIONS: "false", - SHOW_COMMON_EXTENSIONS: "" - } + it("should generate a boolean config", function () { + const input = { + DEEP_LINKING: "true", + SHOW_EXTENSIONS: "false", + SHOW_COMMON_EXTENSIONS: "" + } - expect(translator(input)).toEqual(dedent(` + expect(translator(input)).toEqual(dedent(` deepLinking: true, showExtensions: false, showCommonExtensions: undefined,` - )) - }) + )) + }) - it("should generate an object config", function() { - const input = { - SPEC: `{ swagger: "2.0" }` - } + it("should generate an object config", function () { + const input = { + SPEC: `{ swagger: "2.0" }` + } - expect(translator(input)).toEqual(dedent(` + expect(translator(input)).toEqual(dedent(` spec: { swagger: "2.0" },` ).trim()) - }) + }) - it("should generate an array config", function() { - const input = { - URLS: `["/one", "/two"]`, - SUPPORTED_SUBMIT_METHODS: "" - } + it("should generate an array config", function () { + const input = { + URLS: `["/one", "/two"]`, + SUPPORTED_SUBMIT_METHODS: "" + } - expect(translator(input)).toEqual(dedent(` + expect(translator(input)).toEqual(dedent(` urls: ["/one", "/two"], supportedSubmitMethods: undefined,` ).trim()) - }) + }) - it("should properly escape key names when necessary", function () { - const input = { - URLS: `["/one", "/two"]`, - URLS_PRIMARY_NAME: "one", - } + it("should properly escape key names when necessary", function () { + const input = { + URLS: `["/one", "/two"]`, + URLS_PRIMARY_NAME: "one", + } - expect(translator(input)).toEqual(dedent(` + expect(translator(input)).toEqual(dedent(` urls: ["/one", "/two"], "urls.primaryName": "one",` ).trim()) - }) + }) + + it("should disregard a legacy variable in favor of a regular one", function () { + const input = { + // Order is important to this test... legacy vars should be + // superseded regardless of what is fed in first. + API_URL: "/old.json", + URL: "/swagger.json", + URLS: `["/one", "/two"]`, + API_URLS: `["/three", "/four"]`, + } - it("should disregard a legacy variable in favor of a regular one", function() { - const input = { - // Order is important to this test... legacy vars should be - // superseded regardless of what is fed in first. - API_URL: "/old.json", - URL: "/swagger.json", - URLS: `["/one", "/two"]`, - API_URLS: `["/three", "/four"]`, - } - - expect(translator(input)).toEqual(dedent(` + expect(translator(input)).toEqual(dedent(` url: "/swagger.json", urls: ["/one", "/two"],` ).trim()) - }) + }) + + it("should generate a full config k:v string", function () { + const input = { + API_URL: "/old.yaml", + API_URLS: `["/old", "/older"]`, + CONFIG_URL: "/wow", + DOM_ID: "#swagger_ui", + SPEC: `{ swagger: "2.0" }`, + URL: "/swagger.json", + URLS: `["/one", "/two"]`, + URLS_PRIMARY_NAME: "one", + LAYOUT: "BaseLayout", + DEEP_LINKING: "false", + DISPLAY_OPERATION_ID: "true", + DEFAULT_MODELS_EXPAND_DEPTH: "0", + DEFAULT_MODEL_EXPAND_DEPTH: "1", + DEFAULT_MODEL_RENDERING: "example", + DISPLAY_REQUEST_DURATION: "true", + DOC_EXPANSION: "full", + FILTER: "wowee", + MAX_DISPLAYED_TAGS: "4", + SHOW_EXTENSIONS: "true", + SHOW_COMMON_EXTENSIONS: "false", + OAUTH2_REDIRECT_URL: "http://google.com/", + SHOW_MUTATED_REQUEST: "true", + SUPPORTED_SUBMIT_METHODS: `["get", "post"]`, + VALIDATOR_URL: "http://smartbear.com/" + } - it("should generate a full config k:v string", function() { - const input = { - API_URL: "/old.yaml", - API_URLS: `["/old", "/older"]`, - CONFIG_URL: "/wow", - DOM_ID: "#swagger_ui", - SPEC: `{ swagger: "2.0" }`, - URL: "/swagger.json", - URLS: `["/one", "/two"]`, - URLS_PRIMARY_NAME: "one", - LAYOUT: "BaseLayout", - DEEP_LINKING: "false", - DISPLAY_OPERATION_ID: "true", - DEFAULT_MODELS_EXPAND_DEPTH: "0", - DEFAULT_MODEL_EXPAND_DEPTH: "1", - DEFAULT_MODEL_RENDERING: "example", - DISPLAY_REQUEST_DURATION: "true", - DOC_EXPANSION: "full", - FILTER: "wowee", - MAX_DISPLAYED_TAGS: "4", - SHOW_EXTENSIONS: "true", - SHOW_COMMON_EXTENSIONS: "false", - OAUTH2_REDIRECT_URL: "http://google.com/", - SHOW_MUTATED_REQUEST: "true", - SUPPORTED_SUBMIT_METHODS: `["get", "post"]`, - VALIDATOR_URL: "http://smartbear.com/" - } - - expect(translator(input)).toEqual(dedent(` + expect(translator(input)).toEqual(dedent(` configUrl: "/wow", "dom_id": "#swagger_ui", spec: { swagger: "2.0" }, @@ -165,37 +190,37 @@ describe("docker: env translator", function() { supportedSubmitMethods: ["get", "post"], validatorUrl: "http://smartbear.com/",` ).trim()) - }) + }) + + it("should generate a full config k:v string including base config", function () { + const input = { + API_URL: "/old.yaml", + API_URLS: `["/old", "/older"]`, + CONFIG_URL: "/wow", + DOM_ID: "#swagger_ui", + SPEC: `{ swagger: "2.0" }`, + URL: "/swagger.json", + URLS: `["/one", "/two"]`, + URLS_PRIMARY_NAME: "one", + LAYOUT: "BaseLayout", + DEEP_LINKING: "false", + DISPLAY_OPERATION_ID: "true", + DEFAULT_MODELS_EXPAND_DEPTH: "0", + DEFAULT_MODEL_EXPAND_DEPTH: "1", + DEFAULT_MODEL_RENDERING: "example", + DISPLAY_REQUEST_DURATION: "true", + DOC_EXPANSION: "full", + FILTER: "wowee", + MAX_DISPLAYED_TAGS: "4", + SHOW_EXTENSIONS: "true", + SHOW_COMMON_EXTENSIONS: "false", + OAUTH2_REDIRECT_URL: "http://google.com/", + SHOW_MUTATED_REQUEST: "true", + SUPPORTED_SUBMIT_METHODS: `["get", "post"]`, + VALIDATOR_URL: "http://smartbear.com/" + } - it("should generate a full config k:v string including base config", function() { - const input = { - API_URL: "/old.yaml", - API_URLS: `["/old", "/older"]`, - CONFIG_URL: "/wow", - DOM_ID: "#swagger_ui", - SPEC: `{ swagger: "2.0" }`, - URL: "/swagger.json", - URLS: `["/one", "/two"]`, - URLS_PRIMARY_NAME: "one", - LAYOUT: "BaseLayout", - DEEP_LINKING: "false", - DISPLAY_OPERATION_ID: "true", - DEFAULT_MODELS_EXPAND_DEPTH: "0", - DEFAULT_MODEL_EXPAND_DEPTH: "1", - DEFAULT_MODEL_RENDERING: "example", - DISPLAY_REQUEST_DURATION: "true", - DOC_EXPANSION: "full", - FILTER: "wowee", - MAX_DISPLAYED_TAGS: "4", - SHOW_EXTENSIONS: "true", - SHOW_COMMON_EXTENSIONS: "false", - OAUTH2_REDIRECT_URL: "http://google.com/", - SHOW_MUTATED_REQUEST: "true", - SUPPORTED_SUBMIT_METHODS: `["get", "post"]`, - VALIDATOR_URL: "http://smartbear.com/" - } - - expect(translator(input, { injectBaseConfig: true })).toEqual(dedent(` + expect(translator(input, { injectBaseConfig: true })).toEqual(dedent(` presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset @@ -226,5 +251,6 @@ describe("docker: env translator", function() { supportedSubmitMethods: ["get", "post"], validatorUrl: "http://smartbear.com/",` ).trim()) + }) }) }) \ No newline at end of file