* 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 linebubble
@@ -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 "" | |||
@@ -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") | |||
} |
@@ -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") | |||
} | |||
${afterEndMarkerContent}`) |
@@ -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 `` | |||
} |
@@ -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) { | |||
@@ -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 | |||
@@ -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 | |||
--- | --- | ----- | |||
<a name="initOAuth"></a>`initOAuth` | _Unavailable_ | `(configObj) => void`. Provide Swagger-UI with information about your OAuth server - see the OAuth2 documentation for more information. | |||
<a name="initOAuth"></a>`initOAuth` | [_See `oauth2.md`_](./oauth2.md) | `(configObj) => void`. Provide Swagger-UI with information about your OAuth server - see the OAuth2 documentation for more information. | |||
<a name="preauthorizeBasic"></a>`preauthorizeBasic` | _Unavailable_ | `(authDefinitionKey, username, password) => action`. Programmatically set values for a Basic authorization scheme. | |||
<a name="preauthorizeApiKey"></a>`preauthorizeApiKey` | _Unavailable_ | `(authDefinitionKey, apiKeyValue) => action`. Programmatically set values for an API key authorization scheme. | |||
@@ -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. 🚨** <br>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. 🚨** <br>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({...}) | |||
@@ -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" }, | |||
})`)) | |||
}) | |||
}) |
@@ -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()) | |||
}) | |||
}) | |||
}) |