mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-24 11:26:31 +00:00
Support alternative security scope groups (#3976)
This commit is contained in:
@@ -1047,4 +1047,12 @@ body:has(.openapi-select-popover) {
|
||||
|
||||
.openapi-required-scopes .openapi-required-scopes-description {
|
||||
@apply text-xs !text-tint font-normal mb-2;
|
||||
}
|
||||
|
||||
.openapi-schema-alternatives .openapi-securities-scopes {
|
||||
@apply ml-0 pl-0;
|
||||
}
|
||||
|
||||
.openapi-scopes-alternatives .openapi-schema-alternatives {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
@@ -24,11 +24,19 @@ export function OpenAPIRequiredScopes(props: {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scopes = selectedSecurity.schemes.flatMap((scheme) => {
|
||||
return scheme.scopes ?? [];
|
||||
});
|
||||
const scopeAlternatives =
|
||||
selectedSecurity.scopeAlternatives.length > 0
|
||||
? selectedSecurity.scopeAlternatives
|
||||
: [
|
||||
selectedSecurity.schemes.flatMap((scheme) => {
|
||||
return scheme.scopes ?? [];
|
||||
}),
|
||||
];
|
||||
const resolvedAlternatives = scopeAlternatives
|
||||
.map((scopes) => dedupeScopes(scopes))
|
||||
.filter((scopes) => scopes.length > 0);
|
||||
|
||||
if (!scopes.length) {
|
||||
if (!resolvedAlternatives.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -51,7 +59,12 @@ export function OpenAPIRequiredScopes(props: {
|
||||
{
|
||||
key: 'scopes',
|
||||
label: '',
|
||||
body: <OpenAPISchemaScopes scopes={scopes} context={context} />,
|
||||
body: (
|
||||
<OpenAPIScopeAlternatives
|
||||
alternatives={resolvedAlternatives}
|
||||
context={context}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -60,19 +73,54 @@ export function OpenAPIRequiredScopes(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function OpenAPISchemaScopes(props: {
|
||||
scopes: OpenAPISecurityScope[];
|
||||
function OpenAPIScopeAlternatives(props: {
|
||||
alternatives: OpenAPISecurityScope[][];
|
||||
context: OpenAPIClientContext;
|
||||
}) {
|
||||
const { scopes, context } = props;
|
||||
const { alternatives, context } = props;
|
||||
|
||||
if (alternatives.length === 1) {
|
||||
return <OpenAPISchemaScopes scopes={alternatives[0]} context={context} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="openapi-securities-scopes openapi-markdown">
|
||||
<div className="openapi-scopes-alternatives">
|
||||
<div className="openapi-required-scopes-description">
|
||||
{t(context.translation, 'required_scopes_description')}
|
||||
</div>
|
||||
<div className="openapi-schema-alternatives">
|
||||
{alternatives.map((scopes, index) => (
|
||||
<div key={index} className="openapi-schema-alternative">
|
||||
<OpenAPISchemaScopes scopes={scopes} context={context} hideDescription />
|
||||
{index < alternatives.length - 1 ? (
|
||||
<span className="openapi-schema-alternative-separator">
|
||||
{t(context.translation, 'or')}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OpenAPISchemaScopes(props: {
|
||||
scopes: OpenAPISecurityScope[] | undefined;
|
||||
context: OpenAPIClientContext;
|
||||
isOAuth2?: boolean;
|
||||
hideDescription?: boolean;
|
||||
}) {
|
||||
const { scopes, context, hideDescription } = props;
|
||||
|
||||
return (
|
||||
<div className="openapi-securities-scopes openapi-markdown">
|
||||
{!hideDescription ? (
|
||||
<div className="openapi-required-scopes-description">
|
||||
{t(context.translation, 'required_scopes_description')}
|
||||
</div>
|
||||
) : null}
|
||||
<ul>
|
||||
{scopes.map((scope) => (
|
||||
{scopes?.map((scope) => (
|
||||
<OpenAPIScopeItem key={scope[0]} scope={scope} context={context} />
|
||||
))}
|
||||
</ul>
|
||||
@@ -112,3 +160,18 @@ function OpenAPIScopeItemKey(props: {
|
||||
</OpenAPICopyButton>
|
||||
);
|
||||
}
|
||||
|
||||
function dedupeScopes(scopes: OpenAPISecurityScope[]) {
|
||||
const seen = new Set<string>();
|
||||
const deduped: OpenAPISecurityScope[] = [];
|
||||
|
||||
for (const scope of scopes) {
|
||||
if (seen.has(scope[0])) {
|
||||
continue;
|
||||
}
|
||||
seen.add(scope[0]);
|
||||
deduped.push(scope);
|
||||
}
|
||||
|
||||
return deduped;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export async function resolveOpenAPIOperation(
|
||||
const flatSecurities = flattenSecurities(security);
|
||||
|
||||
// Resolve securities
|
||||
const securities: OpenAPIOperationData['securities'] = [];
|
||||
const securitiesMap = new Map<string, OpenAPIOperationData['securities'][number][1]>();
|
||||
for (const entry of flatSecurities) {
|
||||
const [securityKey, operationScopes] = Object.entries(entry)[0] ?? [];
|
||||
if (securityKey) {
|
||||
@@ -59,14 +59,13 @@ export async function resolveOpenAPIOperation(
|
||||
securityScheme,
|
||||
operationScopes,
|
||||
});
|
||||
securities.push([
|
||||
securityKey,
|
||||
{
|
||||
...securityScheme,
|
||||
required: !isOptionalSecurity,
|
||||
scopes,
|
||||
},
|
||||
]);
|
||||
const existing = securitiesMap.get(securityKey);
|
||||
const mergedScopes = mergeSecurityScopes(existing?.scopes ?? null, scopes);
|
||||
securitiesMap.set(securityKey, {
|
||||
...securityScheme,
|
||||
required: !isOptionalSecurity,
|
||||
scopes: mergedScopes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +74,7 @@ export async function resolveOpenAPIOperation(
|
||||
operation: { ...operation, security },
|
||||
method,
|
||||
path,
|
||||
securities,
|
||||
securities: Array.from(securitiesMap.entries()),
|
||||
'x-codeSamples':
|
||||
typeof schema['x-codeSamples'] === 'boolean' ? schema['x-codeSamples'] : undefined,
|
||||
'x-hideTryItPanel':
|
||||
@@ -199,6 +198,31 @@ function resolveSecurityScopes({
|
||||
return operationScopes.map((scope) => [scope, undefined]);
|
||||
}
|
||||
|
||||
function mergeSecurityScopes(
|
||||
existing: OpenAPISecurityScope[] | null,
|
||||
incoming: OpenAPISecurityScope[] | null
|
||||
): OpenAPISecurityScope[] | null {
|
||||
if (!existing?.length) {
|
||||
return incoming;
|
||||
}
|
||||
if (!incoming?.length) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
const merged: OpenAPISecurityScope[] = [];
|
||||
|
||||
for (const scope of [...existing, ...incoming]) {
|
||||
if (seen.has(scope[0])) {
|
||||
continue;
|
||||
}
|
||||
seen.add(scope[0]);
|
||||
merged.push(scope);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a security scheme is an OAuth or OpenID Connect security scheme.
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,11 @@ import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser'
|
||||
import type { OpenAPIUniversalContext } from './context';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
import { tString } from './translate';
|
||||
import type { OpenAPICustomSecurityScheme, OpenAPIOperationData } from './types';
|
||||
import type {
|
||||
OpenAPICustomSecurityScheme,
|
||||
OpenAPIOperationData,
|
||||
OpenAPISecurityScope,
|
||||
} from './types';
|
||||
|
||||
export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject {
|
||||
return typeof input === 'object' && !!input && '$ref' in input;
|
||||
@@ -327,6 +331,7 @@ export type OperationSecurityInfo = {
|
||||
key: string;
|
||||
label: string;
|
||||
schemes: OpenAPICustomSecurityScheme[];
|
||||
scopeAlternatives: OpenAPISecurityScope[][];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -345,18 +350,64 @@ export function extractOperationSecurityInfo(args: {
|
||||
key,
|
||||
label: key,
|
||||
schemes: [security],
|
||||
scopeAlternatives: security.scopes?.length ? [security.scopes] : [],
|
||||
}));
|
||||
}
|
||||
|
||||
return securityRequirement.map((requirement, idx) => {
|
||||
const schemeKeys = Object.keys(requirement);
|
||||
const grouped = new Map<string, OperationSecurityInfo>();
|
||||
|
||||
return {
|
||||
key: `security-${idx}`,
|
||||
label: schemeKeys.join(' & '),
|
||||
schemes: schemeKeys
|
||||
.map((schemeKey) => securitiesMap.get(schemeKey))
|
||||
.filter((s): s is OpenAPICustomSecurityScheme => s !== undefined),
|
||||
};
|
||||
securityRequirement.forEach((requirement) => {
|
||||
const schemeKeys = Object.keys(requirement).sort();
|
||||
if (schemeKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
const label = schemeKeys.join(' & ');
|
||||
const existing = grouped.get(label);
|
||||
const schemes = schemeKeys
|
||||
.map((schemeKey) => securitiesMap.get(schemeKey))
|
||||
.filter((s): s is OpenAPICustomSecurityScheme => s !== undefined);
|
||||
const scopesForRequirement = schemeKeys.flatMap((schemeKey) =>
|
||||
resolveRequiredScopesForScheme(securitiesMap.get(schemeKey), requirement[schemeKey])
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
existing.scopeAlternatives.push(scopesForRequirement);
|
||||
if (!existing.schemes.length && schemes.length) {
|
||||
existing.schemes = schemes;
|
||||
}
|
||||
} else {
|
||||
grouped.set(label, {
|
||||
key: `security-${grouped.size}`,
|
||||
label,
|
||||
schemes,
|
||||
scopeAlternatives: [scopesForRequirement],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(grouped.values());
|
||||
}
|
||||
|
||||
function resolveRequiredScopesForScheme(
|
||||
security: OpenAPICustomSecurityScheme | undefined,
|
||||
operationScopes: string[] | undefined
|
||||
): OpenAPISecurityScope[] {
|
||||
if (!security || !operationScopes?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (security.type === 'oauth2') {
|
||||
const flows = security.flows ? Object.entries(security.flows) : [];
|
||||
const resolved = flows.flatMap(([_, flow]) => {
|
||||
return Object.entries(flow.scopes ?? {}).filter(([scope]) =>
|
||||
operationScopes.includes(scope)
|
||||
);
|
||||
});
|
||||
|
||||
if (resolved.length) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
return operationScopes.map((scope) => [scope, undefined]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user