feat: add mutualTLS auth option (#9193)
Refs #8020 Co-authored-by: Vladimír Gorej <vladimir.gorej@smartbear.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import OperationLink from "./operation-link"
|
|||||||
import Servers from "./servers"
|
import Servers from "./servers"
|
||||||
import ServersContainer from "./servers-container"
|
import ServersContainer from "./servers-container"
|
||||||
import RequestBodyEditor from "./request-body-editor"
|
import RequestBodyEditor from "./request-body-editor"
|
||||||
import HttpAuth from "./http-auth"
|
import HttpAuth from "./auth/http-auth"
|
||||||
import OperationServers from "./operation-servers"
|
import OperationServers from "./operation-servers"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ export default function () {
|
|||||||
wrapSelectors: authWrapSelectors,
|
wrapSelectors: authWrapSelectors,
|
||||||
},
|
},
|
||||||
oas3: {
|
oas3: {
|
||||||
actions,
|
actions: { ...actions },
|
||||||
reducers,
|
reducers,
|
||||||
selectors,
|
selectors: { ...selectors },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { OAS3ComponentWrapFactory } from "../helpers"
|
import { OAS3ComponentWrapFactory } from "../../helpers"
|
||||||
|
|
||||||
export default OAS3ComponentWrapFactory(({ Ori, ...props }) => {
|
export default OAS3ComponentWrapFactory(({ Ori, ...props }) => {
|
||||||
const {
|
const {
|
||||||
@@ -9,6 +9,7 @@ export default OAS3ComponentWrapFactory(({ Ori, ...props }) => {
|
|||||||
const HttpAuth = getComponent("HttpAuth")
|
const HttpAuth = getComponent("HttpAuth")
|
||||||
const type = schema.get("type")
|
const type = schema.get("type")
|
||||||
|
|
||||||
|
|
||||||
if(type === "http") {
|
if(type === "http") {
|
||||||
return <HttpAuth key={ name }
|
return <HttpAuth key={ name }
|
||||||
schema={ schema }
|
schema={ schema }
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import Markdown from "./markdown"
|
import Markdown from "./markdown"
|
||||||
import AuthItem from "./auth-item"
|
import AuthItem from "./auth/auth-item"
|
||||||
import OnlineValidatorBadge from "./online-validator-badge"
|
import OnlineValidatorBadge from "./online-validator-badge"
|
||||||
import Model from "./model"
|
import Model from "./model"
|
||||||
import JsonSchema_string from "./json-schema-string"
|
import JsonSchema_string from "./json-schema-string"
|
||||||
|
|||||||
28
src/core/plugins/oas31/auth-extensions/wrap-selectors.js
Normal file
28
src/core/plugins/oas31/auth-extensions/wrap-selectors.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* @prettier
|
||||||
|
*/
|
||||||
|
import { Map } from "immutable"
|
||||||
|
import { createOnlyOAS31SelectorWrapper } from "../fn"
|
||||||
|
|
||||||
|
export const definitionsToAuthorize = createOnlyOAS31SelectorWrapper(
|
||||||
|
() => (oriSelector, system) => {
|
||||||
|
const definitions = system.specSelectors.securityDefinitions()
|
||||||
|
let list = oriSelector()
|
||||||
|
|
||||||
|
if (!definitions) return list
|
||||||
|
|
||||||
|
definitions.entrySeq().forEach(([defName, definition]) => {
|
||||||
|
const type = definition.get("type")
|
||||||
|
|
||||||
|
if (type === "mutualTLS") {
|
||||||
|
list = list.push(
|
||||||
|
new Map({
|
||||||
|
[defName]: definition,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
)
|
||||||
184
src/core/plugins/oas31/components/auth/auths.jsx
Normal file
184
src/core/plugins/oas31/components/auth/auths.jsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* @prettier
|
||||||
|
*/
|
||||||
|
import React from "react"
|
||||||
|
import PropTypes from "prop-types"
|
||||||
|
import ImPropTypes from "react-immutable-proptypes"
|
||||||
|
|
||||||
|
class Auths extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
definitions: ImPropTypes.iterable.isRequired,
|
||||||
|
getComponent: PropTypes.func.isRequired,
|
||||||
|
authSelectors: PropTypes.object.isRequired,
|
||||||
|
authActions: PropTypes.object.isRequired,
|
||||||
|
errSelectors: PropTypes.object.isRequired,
|
||||||
|
specSelectors: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context)
|
||||||
|
|
||||||
|
this.state = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
onAuthChange = (auth) => {
|
||||||
|
let { name } = auth
|
||||||
|
|
||||||
|
this.setState({ [name]: auth })
|
||||||
|
}
|
||||||
|
|
||||||
|
submitAuth = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
let { authActions } = this.props
|
||||||
|
authActions.authorizeWithPersistOption(this.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
logoutClick = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
let { authActions, definitions } = this.props
|
||||||
|
let auths = definitions
|
||||||
|
.map((val, key) => {
|
||||||
|
return key
|
||||||
|
})
|
||||||
|
.toArray()
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
auths.reduce((prev, auth) => {
|
||||||
|
prev[auth] = ""
|
||||||
|
return prev
|
||||||
|
}, {})
|
||||||
|
)
|
||||||
|
|
||||||
|
authActions.logoutWithPersistOption(auths)
|
||||||
|
}
|
||||||
|
|
||||||
|
close = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
let { authActions } = this.props
|
||||||
|
|
||||||
|
authActions.showDefinitions(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { definitions, getComponent, authSelectors, errSelectors } = this.props
|
||||||
|
const AuthItem = getComponent("AuthItem")
|
||||||
|
const Oauth2 = getComponent("oauth2", true)
|
||||||
|
const Button = getComponent("Button")
|
||||||
|
|
||||||
|
const authorized = authSelectors.authorized()
|
||||||
|
const authorizedAuth = definitions.filter((definition, key) => {
|
||||||
|
return !!authorized.get(key)
|
||||||
|
})
|
||||||
|
const nonOauthDefinitions = definitions.filter(
|
||||||
|
(schema) =>
|
||||||
|
schema.get("type") !== "oauth2" && schema.get("type") !== "mutualTLS"
|
||||||
|
)
|
||||||
|
const oauthDefinitions = definitions.filter(
|
||||||
|
(schema) => schema.get("type") === "oauth2"
|
||||||
|
)
|
||||||
|
const mutualTLSDefinitions = definitions.filter(
|
||||||
|
(schema) => schema.get("type") === "mutualTLS"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<div className="auth-container">
|
||||||
|
{nonOauthDefinitions.size > 0 && (
|
||||||
|
<form onSubmit={this.submitAuth}>
|
||||||
|
{nonOauthDefinitions
|
||||||
|
.map((schema, name) => {
|
||||||
|
return (
|
||||||
|
<AuthItem
|
||||||
|
key={name}
|
||||||
|
schema={schema}
|
||||||
|
name={name}
|
||||||
|
getComponent={getComponent}
|
||||||
|
onAuthChange={this.onAuthChange}
|
||||||
|
authorized={authorized}
|
||||||
|
errSelectors={errSelectors}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.toArray()}
|
||||||
|
<div className="auth-btn-wrapper">
|
||||||
|
{nonOauthDefinitions.size === authorizedAuth.size ? (
|
||||||
|
<Button
|
||||||
|
className="btn modal-btn auth"
|
||||||
|
onClick={this.logoutClick}
|
||||||
|
aria-label="Remove authorization"
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="btn modal-btn auth authorize"
|
||||||
|
aria-label="Apply credentials"
|
||||||
|
>
|
||||||
|
Authorize
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
className="btn modal-btn auth btn-done"
|
||||||
|
onClick={this.close}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{oauthDefinitions.size > 0 ? (
|
||||||
|
<div>
|
||||||
|
<div className="scope-def">
|
||||||
|
<p>
|
||||||
|
Scopes are used to grant an application different levels of
|
||||||
|
access to data on behalf of the end user. Each API may declare
|
||||||
|
one or more scopes.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
API requires the following scopes. Select which ones you want to
|
||||||
|
grant to Swagger UI.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{definitions
|
||||||
|
.filter((schema) => schema.get("type") === "oauth2")
|
||||||
|
.map((schema, name) => {
|
||||||
|
return (
|
||||||
|
<div key={name}>
|
||||||
|
<Oauth2
|
||||||
|
authorized={authorized}
|
||||||
|
schema={schema}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.toArray()}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{mutualTLSDefinitions.size > 0 && (
|
||||||
|
<div>
|
||||||
|
{mutualTLSDefinitions
|
||||||
|
.map((schema, name) => {
|
||||||
|
return (
|
||||||
|
<AuthItem
|
||||||
|
key={name}
|
||||||
|
schema={schema}
|
||||||
|
name={name}
|
||||||
|
getComponent={getComponent}
|
||||||
|
onAuthChange={this.onAuthChange}
|
||||||
|
authorized={authorized}
|
||||||
|
errSelectors={errSelectors}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.toArray()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Auths
|
||||||
29
src/core/plugins/oas31/components/auth/mutual-tls-auth.jsx
Normal file
29
src/core/plugins/oas31/components/auth/mutual-tls-auth.jsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* @prettier
|
||||||
|
*/
|
||||||
|
import React from "react"
|
||||||
|
import PropTypes from "prop-types"
|
||||||
|
|
||||||
|
const MutualTLSAuth = ({ schema, getComponent }) => {
|
||||||
|
const JumpToPath = getComponent("JumpToPath", true)
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h4>
|
||||||
|
{schema.get("name")} (mutualTLS){" "}
|
||||||
|
<JumpToPath path={["securityDefinitions", schema.get("name")]} />
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
Mutual TLS is required by this API/Operation. Certificates are managed
|
||||||
|
via your Operating System and/or your browser.
|
||||||
|
</p>
|
||||||
|
<p>{schema.get("description")}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
MutualTLSAuth.propTypes = {
|
||||||
|
schema: PropTypes.object.isRequired,
|
||||||
|
getComponent: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MutualTLSAuth
|
||||||
@@ -9,12 +9,16 @@ import JsonSchemaDialect from "./components/json-schema-dialect"
|
|||||||
import VersionPragmaFilter from "./components/version-pragma-filter"
|
import VersionPragmaFilter from "./components/version-pragma-filter"
|
||||||
import Model from "./components/model/model"
|
import Model from "./components/model/model"
|
||||||
import Models from "./components/models/models"
|
import Models from "./components/models/models"
|
||||||
|
import MutualTLSAuth from "./components/auth/mutual-tls-auth"
|
||||||
|
import Auths from "./components/auth/auths"
|
||||||
import LicenseWrapper from "./wrap-components/license"
|
import LicenseWrapper from "./wrap-components/license"
|
||||||
import ContactWrapper from "./wrap-components/contact"
|
import ContactWrapper from "./wrap-components/contact"
|
||||||
import InfoWrapper from "./wrap-components/info"
|
import InfoWrapper from "./wrap-components/info"
|
||||||
import ModelWrapper from "./wrap-components/model"
|
import ModelWrapper from "./wrap-components/model"
|
||||||
import ModelsWrapper from "./wrap-components/models"
|
import ModelsWrapper from "./wrap-components/models"
|
||||||
import VersionPragmaFilterWrapper from "./wrap-components/version-pragma-filter"
|
import VersionPragmaFilterWrapper from "./wrap-components/version-pragma-filter"
|
||||||
|
import AuthItemWrapper from "./wrap-components/auth/auth-item"
|
||||||
|
import AuthsWrapper from "./wrap-components/auths"
|
||||||
import {
|
import {
|
||||||
isOAS31 as isOAS31Fn,
|
isOAS31 as isOAS31Fn,
|
||||||
createOnlyOAS31Selector as createOnlyOAS31SelectorFn,
|
createOnlyOAS31Selector as createOnlyOAS31SelectorFn,
|
||||||
@@ -50,6 +54,7 @@ import {
|
|||||||
isOAS3 as isOAS3SelectorWrapper,
|
isOAS3 as isOAS3SelectorWrapper,
|
||||||
selectLicenseUrl as selectLicenseUrlWrapper,
|
selectLicenseUrl as selectLicenseUrlWrapper,
|
||||||
} from "./spec-extensions/wrap-selectors"
|
} from "./spec-extensions/wrap-selectors"
|
||||||
|
import { definitionsToAuthorize as definitionsToAuthorizeWrapper } from "./auth-extensions/wrap-selectors"
|
||||||
import { selectLicenseUrl as selectOAS31LicenseUrl } from "./selectors"
|
import { selectLicenseUrl as selectOAS31LicenseUrl } from "./selectors"
|
||||||
import JSONSchema202012KeywordExample from "./json-schema-2020-12-extensions/components/keywords/Example"
|
import JSONSchema202012KeywordExample from "./json-schema-2020-12-extensions/components/keywords/Example"
|
||||||
import JSONSchema202012KeywordXml from "./json-schema-2020-12-extensions/components/keywords/Xml"
|
import JSONSchema202012KeywordXml from "./json-schema-2020-12-extensions/components/keywords/Xml"
|
||||||
@@ -74,12 +79,14 @@ const OAS31Plugin = ({ fn }) => {
|
|||||||
components: {
|
components: {
|
||||||
Webhooks,
|
Webhooks,
|
||||||
JsonSchemaDialect,
|
JsonSchemaDialect,
|
||||||
|
MutualTLSAuth,
|
||||||
OAS31Info: Info,
|
OAS31Info: Info,
|
||||||
OAS31License: License,
|
OAS31License: License,
|
||||||
OAS31Contact: Contact,
|
OAS31Contact: Contact,
|
||||||
OAS31VersionPragmaFilter: VersionPragmaFilter,
|
OAS31VersionPragmaFilter: VersionPragmaFilter,
|
||||||
OAS31Model: Model,
|
OAS31Model: Model,
|
||||||
OAS31Models: Models,
|
OAS31Models: Models,
|
||||||
|
OAS31Auths: Auths,
|
||||||
JSONSchema202012KeywordExample,
|
JSONSchema202012KeywordExample,
|
||||||
JSONSchema202012KeywordXml,
|
JSONSchema202012KeywordXml,
|
||||||
JSONSchema202012KeywordDiscriminator,
|
JSONSchema202012KeywordDiscriminator,
|
||||||
@@ -92,6 +99,8 @@ const OAS31Plugin = ({ fn }) => {
|
|||||||
VersionPragmaFilter: VersionPragmaFilterWrapper,
|
VersionPragmaFilter: VersionPragmaFilterWrapper,
|
||||||
Model: ModelWrapper,
|
Model: ModelWrapper,
|
||||||
Models: ModelsWrapper,
|
Models: ModelsWrapper,
|
||||||
|
AuthItem: AuthItemWrapper,
|
||||||
|
auths: AuthsWrapper,
|
||||||
JSONSchema202012KeywordDescription:
|
JSONSchema202012KeywordDescription:
|
||||||
JSONSchema202012KeywordDescriptionWrapper,
|
JSONSchema202012KeywordDescriptionWrapper,
|
||||||
JSONSchema202012KeywordDefault: JSONSchema202012KeywordDefaultWrapper,
|
JSONSchema202012KeywordDefault: JSONSchema202012KeywordDefaultWrapper,
|
||||||
@@ -99,6 +108,11 @@ const OAS31Plugin = ({ fn }) => {
|
|||||||
JSONSchema202012KeywordPropertiesWrapper,
|
JSONSchema202012KeywordPropertiesWrapper,
|
||||||
},
|
},
|
||||||
statePlugins: {
|
statePlugins: {
|
||||||
|
auth: {
|
||||||
|
wrapSelectors: {
|
||||||
|
definitionsToAuthorize: definitionsToAuthorizeWrapper,
|
||||||
|
},
|
||||||
|
},
|
||||||
spec: {
|
spec: {
|
||||||
selectors: {
|
selectors: {
|
||||||
isOAS31: createSystemSelector(selectIsOAS31),
|
isOAS31: createSystemSelector(selectIsOAS31),
|
||||||
|
|||||||
22
src/core/plugins/oas31/wrap-components/auth/auth-item.jsx
Normal file
22
src/core/plugins/oas31/wrap-components/auth/auth-item.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* @prettier
|
||||||
|
*/
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
import { createOnlyOAS31ComponentWrapper } from "../../fn"
|
||||||
|
|
||||||
|
const AuthItem = createOnlyOAS31ComponentWrapper(
|
||||||
|
({ originalComponent: Ori, ...props }) => {
|
||||||
|
const { getComponent, schema } = props
|
||||||
|
const MutualTLSAuth = getComponent("MutualTLSAuth", true)
|
||||||
|
const type = schema.get("type")
|
||||||
|
|
||||||
|
if (type === "mutualTLS") {
|
||||||
|
return <MutualTLSAuth schema={schema} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Ori {...props} />
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default AuthItem
|
||||||
17
src/core/plugins/oas31/wrap-components/auths.jsx
Normal file
17
src/core/plugins/oas31/wrap-components/auths.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* @prettier
|
||||||
|
*/
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
import { createOnlyOAS31ComponentWrapper } from "../fn"
|
||||||
|
|
||||||
|
const AuthsWrapper = createOnlyOAS31ComponentWrapper(
|
||||||
|
({ getSystem, ...props }) => {
|
||||||
|
const system = getSystem()
|
||||||
|
const OAS31Auths = system.getComponent("OAS31Auths", true)
|
||||||
|
|
||||||
|
return <OAS31Auths {...props} />
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default AuthsWrapper
|
||||||
30
test/e2e-cypress/e2e/features/oas31-auth-mutual-tls.cy.js
Normal file
30
test/e2e-cypress/e2e/features/oas31-auth-mutual-tls.cy.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
describe("MutualTLS Authorization", () => {
|
||||||
|
|
||||||
|
it("should open authorization popup", () => {
|
||||||
|
cy.visit(
|
||||||
|
"/?url=/documents/security/mutual-tls.yaml"
|
||||||
|
)
|
||||||
|
.get("button.authorize")
|
||||||
|
.click()
|
||||||
|
.get(".auth-container h4")
|
||||||
|
.contains("mutual (mutualTLS)")
|
||||||
|
})
|
||||||
|
it("should have description given by user", () => {
|
||||||
|
cy.visit(
|
||||||
|
"/?url=/documents/security/mutual-tls.yaml"
|
||||||
|
)
|
||||||
|
.get("button.authorize")
|
||||||
|
.click()
|
||||||
|
.get(".auth-container p:nth-of-type(2)")
|
||||||
|
.contains("Mutual TLS description")
|
||||||
|
})
|
||||||
|
it("should not display Authorize or Logout buttons", () => {
|
||||||
|
cy.visit(
|
||||||
|
"/?url=/documents/security/mutual-tls.yaml"
|
||||||
|
)
|
||||||
|
.get("button.authorize")
|
||||||
|
.click()
|
||||||
|
.get(".auth-button-wrapper")
|
||||||
|
.should("not.exist")
|
||||||
|
})
|
||||||
|
})
|
||||||
10
test/e2e-cypress/static/documents/security/mutual-tls.yaml
Normal file
10
test/e2e-cypress/static/documents/security/mutual-tls.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: Test Mutual TLS authorization
|
||||||
|
version: 1.0.0
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
mutual:
|
||||||
|
type: mutualTLS
|
||||||
|
description: Mutual TLS description
|
||||||
|
name: mutual
|
||||||
Reference in New Issue
Block a user