feat: add mutualTLS auth option (#9193)

Refs #8020 

Co-authored-by: Vladimír Gorej <vladimir.gorej@smartbear.com>
This commit is contained in:
Patryk Rosiak
2023-09-13 15:32:35 +02:00
committed by GitHub
parent 8db1226d1e
commit 89cdd7b022
13 changed files with 340 additions and 5 deletions

View File

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

View File

@@ -23,9 +23,9 @@ export default function () {
wrapSelectors: authWrapSelectors,
},
oas3: {
actions,
actions: { ...actions },
reducers,
selectors,
selectors: { ...selectors },
},
},
}

View File

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

View File

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

View 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
}
)

View 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

View 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

View File

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

View 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

View 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

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

View 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