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 ServersContainer from "./servers-container"
|
||||
import RequestBodyEditor from "./request-body-editor"
|
||||
import HttpAuth from "./http-auth"
|
||||
import HttpAuth from "./auth/http-auth"
|
||||
import OperationServers from "./operation-servers"
|
||||
|
||||
export default {
|
||||
|
||||
@@ -23,9 +23,9 @@ export default function () {
|
||||
wrapSelectors: authWrapSelectors,
|
||||
},
|
||||
oas3: {
|
||||
actions,
|
||||
actions: { ...actions },
|
||||
reducers,
|
||||
selectors,
|
||||
selectors: { ...selectors },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react"
|
||||
import { OAS3ComponentWrapFactory } from "../helpers"
|
||||
import { OAS3ComponentWrapFactory } from "../../helpers"
|
||||
|
||||
export default OAS3ComponentWrapFactory(({ Ori, ...props }) => {
|
||||
const {
|
||||
@@ -9,6 +9,7 @@ export default OAS3ComponentWrapFactory(({ Ori, ...props }) => {
|
||||
const HttpAuth = getComponent("HttpAuth")
|
||||
const type = schema.get("type")
|
||||
|
||||
|
||||
if(type === "http") {
|
||||
return <HttpAuth key={ name }
|
||||
schema={ schema }
|
||||
@@ -1,5 +1,5 @@
|
||||
import Markdown from "./markdown"
|
||||
import AuthItem from "./auth-item"
|
||||
import AuthItem from "./auth/auth-item"
|
||||
import OnlineValidatorBadge from "./online-validator-badge"
|
||||
import Model from "./model"
|
||||
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 Model from "./components/model/model"
|
||||
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 ContactWrapper from "./wrap-components/contact"
|
||||
import InfoWrapper from "./wrap-components/info"
|
||||
import ModelWrapper from "./wrap-components/model"
|
||||
import ModelsWrapper from "./wrap-components/models"
|
||||
import VersionPragmaFilterWrapper from "./wrap-components/version-pragma-filter"
|
||||
import AuthItemWrapper from "./wrap-components/auth/auth-item"
|
||||
import AuthsWrapper from "./wrap-components/auths"
|
||||
import {
|
||||
isOAS31 as isOAS31Fn,
|
||||
createOnlyOAS31Selector as createOnlyOAS31SelectorFn,
|
||||
@@ -50,6 +54,7 @@ import {
|
||||
isOAS3 as isOAS3SelectorWrapper,
|
||||
selectLicenseUrl as selectLicenseUrlWrapper,
|
||||
} from "./spec-extensions/wrap-selectors"
|
||||
import { definitionsToAuthorize as definitionsToAuthorizeWrapper } from "./auth-extensions/wrap-selectors"
|
||||
import { selectLicenseUrl as selectOAS31LicenseUrl } from "./selectors"
|
||||
import JSONSchema202012KeywordExample from "./json-schema-2020-12-extensions/components/keywords/Example"
|
||||
import JSONSchema202012KeywordXml from "./json-schema-2020-12-extensions/components/keywords/Xml"
|
||||
@@ -74,12 +79,14 @@ const OAS31Plugin = ({ fn }) => {
|
||||
components: {
|
||||
Webhooks,
|
||||
JsonSchemaDialect,
|
||||
MutualTLSAuth,
|
||||
OAS31Info: Info,
|
||||
OAS31License: License,
|
||||
OAS31Contact: Contact,
|
||||
OAS31VersionPragmaFilter: VersionPragmaFilter,
|
||||
OAS31Model: Model,
|
||||
OAS31Models: Models,
|
||||
OAS31Auths: Auths,
|
||||
JSONSchema202012KeywordExample,
|
||||
JSONSchema202012KeywordXml,
|
||||
JSONSchema202012KeywordDiscriminator,
|
||||
@@ -92,6 +99,8 @@ const OAS31Plugin = ({ fn }) => {
|
||||
VersionPragmaFilter: VersionPragmaFilterWrapper,
|
||||
Model: ModelWrapper,
|
||||
Models: ModelsWrapper,
|
||||
AuthItem: AuthItemWrapper,
|
||||
auths: AuthsWrapper,
|
||||
JSONSchema202012KeywordDescription:
|
||||
JSONSchema202012KeywordDescriptionWrapper,
|
||||
JSONSchema202012KeywordDefault: JSONSchema202012KeywordDefaultWrapper,
|
||||
@@ -99,6 +108,11 @@ const OAS31Plugin = ({ fn }) => {
|
||||
JSONSchema202012KeywordPropertiesWrapper,
|
||||
},
|
||||
statePlugins: {
|
||||
auth: {
|
||||
wrapSelectors: {
|
||||
definitionsToAuthorize: definitionsToAuthorizeWrapper,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
selectors: {
|
||||
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