Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions src/core/components/auth/scope-display.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React from "react"
import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes"

export default class ScopeDisplay extends React.Component {
static propTypes = {
security: ImPropTypes.iterable,
authSelectors: PropTypes.object.isRequired,
authDefinitions: ImPropTypes.iterable,
specSelectors: PropTypes.object.isRequired
}

extractSecurityRequirements = (security) => {
if (!security || !security.count()) {
return null
}

const requirements = []

// Each item in security array represents an OR condition
security.forEach((requirement) => {
const schemes = []

// Each entry in a requirement represents an AND condition
requirement.forEach((scopes, schemeName) => {
const schemeData = {
name: schemeName,
scopes: []
}

// Handle different security scheme types
if (scopes && scopes.size > 0) {
// For OAuth2, OpenID Connect, or any scheme with scopes
schemeData.scopes = scopes.toJS()
}

schemes.push(schemeData)
})

requirements.push(schemes)
})

return requirements
}

formatNonOptionalRequirements = (requirements) => {
return requirements.map((requirementGroup, idx) => {
const isLastGroup = idx === requirements.length - 1

return (
<span key={idx} className="opblock-security-requirement-group">
{requirementGroup.map((scheme, schemeIdx) => {
const isLastInGroup = schemeIdx === requirementGroup.length - 1

return (
<span key={schemeIdx} className="opblock-security-requirement">
<span className="opblock-security-scheme-name">{scheme.name}</span>
{scheme.scopes.length > 0 && (
<span className="opblock-security-scope-list">
{" ("}
{scheme.scopes.map((scope, scopeIdx) => (
<React.Fragment key={scopeIdx}>
<code className="opblock-security-scope">
{scope}
</code>
{scopeIdx < scheme.scopes.length - 1 ? ", " : ""}
</React.Fragment>
))}
{")"}
</span>
)}
{!isLastInGroup && (
<span className="opblock-security-operator"> + </span>
)}
</span>
)
})}
{!isLastGroup && (
<span className="opblock-security-operator"> OR </span>
)}
</span>
)
})
}

formatSecurityDisplay = (requirements) => {
if (!requirements || requirements.length === 0) {
return null
}

// Check if this is optional security (empty object in array)
if (requirements.length === 1 && requirements[0].length === 0) {
return <span className="opblock-security-requirement opblock-security-optional">Optional</span>
}

// Check for optional security pattern (one empty and others with auth)
const hasEmptyRequirement = requirements.some(req => req.length === 0)
const hasNonEmptyRequirement = requirements.some(req => req.length > 0)

if (hasEmptyRequirement && hasNonEmptyRequirement) {
// Filter out empty requirements and add optional label
const nonEmptyRequirements = requirements.filter(req => req.length > 0)
return (
<React.Fragment>
<span className="opblock-security-requirement opblock-security-optional">Optional</span>
<span className="opblock-security-operator"> OR </span>
{this.formatNonOptionalRequirements(nonEmptyRequirements)}
</React.Fragment>
)
}

return this.formatNonOptionalRequirements(requirements)
}

render() {
const { security } = this.props
const requirements = this.extractSecurityRequirements(security)
const display = this.formatSecurityDisplay(requirements)

if (!display) {
return null
}

return (
<div className="opblock-security-display">
{display}
</div>
)
}
}
25 changes: 18 additions & 7 deletions src/core/components/operation-summary.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default class OperationSummary extends PureComponent {
getConfigs: PropTypes.func.isRequired,
authActions: PropTypes.object,
authSelectors: PropTypes.object,
specSelectors: PropTypes.object,
}

static defaultProps = {
Expand All @@ -32,6 +33,7 @@ export default class OperationSummary extends PureComponent {
getComponent,
authActions,
authSelectors,
specSelectors,
operationProps,
specPath,
} = this.props
Expand All @@ -55,6 +57,7 @@ export default class OperationSummary extends PureComponent {
let security = operationProps.get("security")

const AuthorizeOperationBtn = getComponent("authorizeOperationBtn", true)
const ScopeDisplay = getComponent("ScopeDisplay", true)
const OperationSummaryMethod = getComponent("OperationSummaryMethod")
const OperationSummaryPath = getComponent("OperationSummaryPath")
const JumpToPath = getComponent("JumpToPath", true)
Expand Down Expand Up @@ -88,13 +91,21 @@ export default class OperationSummary extends PureComponent {
<CopyToClipboardBtn textToCopy={`${specPath.get(1)}`} />
{
allowAnonymous ? null :
<AuthorizeOperationBtn
isAuthorized={isAuthorized}
onClick={() => {
const applicableDefinitions = authSelectors.definitionsForRequirements(security)
authActions.showDefinitions(applicableDefinitions)
}}
/>
<div className="opblock-auth-wrapper">
<ScopeDisplay
security={security}
authSelectors={authSelectors}
authDefinitions={authSelectors.definitionsToAuthorize()}
specSelectors={specSelectors}
/>
<AuthorizeOperationBtn
isAuthorized={isAuthorized}
onClick={() => {
const applicableDefinitions = authSelectors.definitionsForRequirements(security)
authActions.showDefinitions(applicableDefinitions)
}}
/>
</div>
}
<JumpToPath path={specPath} />{/* TODO: use wrapComponents here, swagger-ui doesn't care about jumpToPath */}
<button
Expand Down
49 changes: 49 additions & 0 deletions src/core/plugins/auth/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,52 @@ export const getConfigs = createSelector(
state,
auth => auth.get( "configs" )
)

export const getSecurityRequirementsForOperation = (state, securities) => ({ specSelectors }) => {
if (!securities || !securities.count()) {
return null
}

const securityDefinitions = specSelectors.securityDefinitions() || Map({})
const requirements = []

securities.forEach((requirement) => {
const schemes = []

requirement.forEach((scopes, schemeName) => {
const definition = securityDefinitions.get(schemeName)

if (definition) {
const schemeInfo = {
name: schemeName,
type: definition.get("type"),
scopes: scopes ? scopes.toJS() : [],
description: definition.get("description")
}

// Add scheme-specific metadata
if (definition.get("type") === "oauth2") {
schemeInfo.flow = definition.get("flow")
schemeInfo.authorizationUrl = definition.get("authorizationUrl")
schemeInfo.tokenUrl = definition.get("tokenUrl")
} else if (definition.get("type") === "openIdConnect") {
schemeInfo.openIdConnectUrl = definition.get("openIdConnectUrl")
} else if (definition.get("type") === "apiKey") {
schemeInfo.in = definition.get("in")
schemeInfo.name = definition.get("name")
} else if (definition.get("type") === "http") {
schemeInfo.scheme = definition.get("scheme")
schemeInfo.bearerFormat = definition.get("bearerFormat")
}

schemes.push(schemeInfo)
}
})

if (schemes.length > 0) {
requirements.push(schemes)
}
})

return requirements
}
2 changes: 2 additions & 0 deletions src/core/presets/base/plugins/core-components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import AuthorizationPopup from "core/components/auth/authorization-popup"
import AuthorizeBtn from "core/components/auth/authorize-btn"
import AuthorizeBtnContainer from "core/containers/authorize-btn"
import AuthorizeOperationBtn from "core/components/auth/authorize-operation-btn"
import ScopeDisplay from "core/components/auth/scope-display"
import Auths from "core/components/auth/auths"
import AuthItem from "core/components/auth/auth-item"
import AuthError from "core/components/auth/error"
Expand Down Expand Up @@ -68,6 +69,7 @@ const CoreComponentsPlugin = () => ({
authorizeBtn: AuthorizeBtn,
AuthorizeBtnContainer,
authorizeOperationBtn: AuthorizeOperationBtn,
ScopeDisplay,
auths: Auths,
AuthItem: AuthItem,
authError: AuthError,
Expand Down
95 changes: 95 additions & 0 deletions src/style/_authorize.scss
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,98 @@
.scope-def {
padding: 0 0 20px 0;
}

// Security requirements display in operation summary
.opblock-auth-wrapper {
display: inline-flex;
align-items: center;
gap: 5px;

// Add left padding to scope display (replaces the auth button's padding)
.opblock-security-display:first-child {
padding-left: 10px;
}

// Remove left padding from auth button when preceded by scope display
.opblock-security-display + .authorization__btn {
padding-left: 0;
}
}

.opblock-security-display {
display: inline-flex;
align-items: center;
font-size: 12px;
flex-wrap: wrap;
gap: 4px;
}

.opblock-security-requirement-group {
display: inline-flex;
align-items: center;
gap: 4px;
}

.opblock-security-requirement {
display: inline-flex;
align-items: center;
background-color: rgba(#61affe, 0.1);
padding: 2px 8px;
border-radius: 4px;
border: 1px solid rgba(#61affe, 0.3);
}

.opblock-security-optional {
background-color: rgba(#73d13d, 0.1);
border-color: rgba(#73d13d, 0.3);
color: #52c41a;
font-style: italic;
}

.opblock-security-scheme-name {
font-weight: 600;
color: #1890ff;
margin-right: 2px;
}

.opblock-security-scope-list {
display: inline-flex;
align-items: center;
color: #595959;
}

.opblock-security-scope {
background-color: #f0f0f0;
padding: 1px 4px;
border-radius: 2px;
font-size: 11px;
margin: 0 2px;
color: #333;
font-family: monospace;
}

.opblock-security-operator {
color: #8c8c8c;
font-weight: 600;
padding: 0 4px;
font-size: 11px;
text-transform: uppercase;
}

// Responsive design for mobile
@media (max-width: 768px) {
.opblock-auth-wrapper {
display: block;
margin-top: 5px;
}

.opblock-security-display {
display: block;
margin-top: 5px;
}

.opblock-security-requirement-group {
display: block;
margin-bottom: 4px;
}
}
Loading