Browse Source

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
bubble
kyle 5 years ago
committed by GitHub
parent
commit
c6eb8edb5f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 286 additions and 162 deletions
  1. +0
    -5
      Dockerfile
  2. +13
    -0
      docker/configurator/helpers.js
  3. +5
    -16
      docker/configurator/index.js
  4. +43
    -0
      docker/configurator/oauth.js
  5. +4
    -0
      docker/configurator/translator.js
  6. +0
    -7
      docker/run.sh
  7. +4
    -2
      docs/usage/configuration.md
  8. +9
    -8
      docs/usage/oauth2.md
  9. +58
    -0
      test/docker/oauth.js
  10. +150
    -124
      test/docker/translator.js

+ 0
- 5
Dockerfile View File

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


+ 13
- 0
docker/configurator/helpers.js View File

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

+ 5
- 16
docker/configurator/index.js View File

@@ -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}`)

+ 43
- 0
docker/configurator/oauth.js View File

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

+ 4
- 0
docker/configurator/translator.js View File

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


+ 0
- 7
docker/run.sh View File

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


+ 4
- 2
docs/usage/configuration.md View File

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



+ 9
- 8
docs/usage/oauth2.md View File

@@ -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({...})


+ 58
- 0
test/docker/oauth.js View File

@@ -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" },
})`))
})
})

+ 150
- 124
test/docker/translator.js View File

@@ -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())
})
})
})

Loading…
Cancel
Save