Enhance OpenAPI security scopes handling (#3712)

This commit is contained in:
Nolann B.
2025-10-09 10:16:12 +02:00
committed by GitHub
parent 4f9abfb6f2
commit eea8f1e00f
16 changed files with 154 additions and 67 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---
Enhance OpenAPI security scopes handling
@@ -317,10 +317,11 @@
}
.openapi-securities-oauth-flows {
@apply flex flex-col gap-2 divide-y divide-tint-subtle;
@apply flex flex-col gap-3;
}
.openapi-securities-oauth-content {
.openapi-securities-oauth-content,
.openapi-securities-scopes {
@apply prose *:!prose-sm *:text-tint;
}
@@ -328,7 +329,7 @@
@apply text-xs;
}
.openapi-securities-oauth-content ul {
.openapi-securities-scopes ul {
@apply !my-0;
}
@@ -1,11 +1,12 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { Fragment } from 'react';
import { InteractiveSection } from './InteractiveSection';
import { Markdown } from './Markdown';
import { OpenAPICopyButton } from './OpenAPICopyButton';
import { OpenAPISchemaName } from './OpenAPISchemaName';
import type { OpenAPIClientContext } from './context';
import { t } from './translate';
import type { OpenAPISecuritySchemeWithRequired } from './types';
import type { OpenAPICustomSecurityScheme, OpenAPISecurityScope } from './types';
import type { OpenAPIOperationData } from './types';
import { createStateKey, extractOperationSecurityInfo, resolveDescription } from './utils';
@@ -53,6 +54,12 @@ export function OpenAPISecurities(props: {
className="openapi-securities-description"
/>
) : null}
{security.scopes?.length ? (
<OpenAPISchemaScopes
scopes={security.scopes}
context={context}
/>
) : null}
</div>
);
})}
@@ -63,10 +70,7 @@ export function OpenAPISecurities(props: {
);
}
function getLabelForType(
security: OpenAPISecuritySchemeWithRequired,
context: OpenAPIClientContext
) {
function getLabelForType(security: OpenAPICustomSecurityScheme, context: OpenAPIClientContext) {
switch (security.type) {
case 'apiKey':
return (
@@ -90,7 +94,6 @@ function getLabelForType(
}
if (security.scheme === 'bearer') {
const description = resolveDescription(security);
return (
<>
<OpenAPISchemaName
@@ -100,7 +103,7 @@ function getLabelForType(
required={security.required}
/>
{/** Show a default description if none is provided */}
{!description ? (
{!security.description ? (
<Markdown
source={`Bearer authentication header of the form Bearer ${'&lt;token&gt;'}.`}
className="openapi-securities-description"
@@ -139,18 +142,20 @@ function OpenAPISchemaOAuth2Flows(props: {
}) {
const { context, security } = props;
const flows = Object.entries(security.flows ?? {});
const flows = security.flows ? Object.entries(security.flows) : [];
return (
<div className="openapi-securities-oauth-flows">
{flows.map(([name, flow], index) => (
<OpenAPISchemaOAuth2Item
key={index}
flow={flow}
name={name}
context={context}
security={security}
/>
<Fragment key={index}>
<OpenAPISchemaOAuth2Item
flow={flow}
name={name}
context={context}
security={security}
/>
{index < flows.length - 1 ? <hr /> : null}
</Fragment>
))}
</div>
);
@@ -170,7 +175,7 @@ function OpenAPISchemaOAuth2Item(props: {
return null;
}
const scopes = Object.entries(flow?.scopes ?? {});
const scopes = flow.scopes ? Object.entries(flow.scopes) : [];
return (
<div>
@@ -221,22 +226,62 @@ function OpenAPISchemaOAuth2Item(props: {
</OpenAPICopyButton>
</span>
) : null}
{scopes.length ? (
<div>
{t(context.translation, 'available_scopes')}:{' '}
<ul>
{scopes.map(([key, value]) => (
<li key={key}>
<OpenAPICopyButton value={key} context={context} withTooltip>
<code>{key}</code>
</OpenAPICopyButton>
: {value}
</li>
))}
</ul>
</div>
) : null}
{scopes.length ? <OpenAPISchemaScopes scopes={scopes} context={context} /> : null}
</div>
</div>
);
}
/**
* Render a list of available scopes.
*/
function OpenAPISchemaScopes(props: {
scopes: OpenAPISecurityScope[];
context: OpenAPIClientContext;
}) {
const { scopes, context } = props;
return (
<div className="openapi-securities-scopes openapi-markdown">
<span>{t(context.translation, 'required_scopes')}: </span>
<ul>
{scopes.map((scope) => (
<OpenAPIScopeItem key={scope[0]} scope={scope} context={context} />
))}
</ul>
</div>
);
}
/**
* Display a scope item. Either a key-value pair or a single string.
*/
function OpenAPIScopeItem(props: {
scope: OpenAPISecurityScope;
context: OpenAPIClientContext;
}) {
const { scope, context } = props;
return (
<li>
<OpenAPIScopeItemKey name={scope[0]} context={context} />
{scope[1] ? `: ${scope[1]}` : null}
</li>
);
}
/**
* Displays the scope name within a copyable button.
*/
function OpenAPIScopeItemKey(props: {
name: string;
context: OpenAPIClientContext;
}) {
const { name, context } = props;
return (
<OpenAPICopyButton value={name} context={context} withTooltip>
<code>{name}</code>
</OpenAPICopyButton>
);
}
@@ -7,7 +7,7 @@ import type {
OpenAPIV3xDocument,
} from '@gitbook/openapi-parser';
import { dereferenceFilesystem } from './dereference';
import type { OpenAPIOperationData } from './types';
import type { OpenAPIOperationData, OpenAPISecurityScope } from './types';
import { checkIsReference } from './utils';
export { fromJSON, toJSON };
@@ -54,18 +54,21 @@ export async function resolveOpenAPIOperation(
// Resolve securities
const securities: OpenAPIOperationData['securities'] = [];
for (const entry of flatSecurities) {
const securityKey = Object.keys(entry)[0];
const [securityKey, operationScopes] = Object.entries(entry)[0] ?? [];
if (securityKey) {
const securityScheme = schema.components?.securitySchemes?.[securityKey];
if (securityScheme && !checkIsReference(securityScheme)) {
securities.push([
securityKey,
{
...securityScheme,
required: !isOptionalSecurity,
},
]);
}
const scopes = resolveSecurityScopes({
securityScheme,
operationScopes,
});
securities.push([
securityKey,
{
...securityScheme,
required: !isOptionalSecurity,
scopes,
},
]);
}
}
@@ -91,10 +94,7 @@ function getPathObject(
schema: OpenAPIV3.Document | OpenAPIV3_1.Document,
path: string
): OpenAPIV3.PathItemObject | OpenAPIV3_1.PathItemObject | null {
if (schema.paths?.[path]) {
return schema.paths[path];
}
return null;
return schema.paths?.[path] || null;
}
/**
@@ -149,3 +149,33 @@ function flattenSecurities(security: OpenAPIV3.SecurityRequirementObject[]) {
}));
});
}
/**
* Resolve the scopes for a security scheme.
*/
function resolveSecurityScopes({
securityScheme,
operationScopes,
}: {
securityScheme?: OpenAPIV3.ReferenceObject | OpenAPIV3.SecuritySchemeObject;
operationScopes?: string[];
}): OpenAPISecurityScope[] | null {
if (
!securityScheme ||
checkIsReference(securityScheme) ||
isOAuthSecurityScheme(securityScheme)
) {
return null;
}
return operationScopes?.map((scope) => [scope, undefined]) || [];
}
/**
* Check if a security scheme is an OAuth or OpenID Connect security scheme.
*/
function isOAuthSecurityScheme(
securityScheme: OpenAPIV3.SecuritySchemeObject
): securityScheme is OpenAPIV3.OAuth2SecurityScheme {
return securityScheme.type === 'oauth2';
}
@@ -36,7 +36,7 @@ export const de = {
show: 'Zeige ${1}',
hide: 'Verstecke ${1}',
available_items: 'Verfügbare Elemente',
available_scopes: 'Verfügbare scopes',
required_scopes: 'Erforderliche Scopes',
properties: 'Eigenschaften',
or: 'oder',
and: 'und',
@@ -36,7 +36,7 @@ export const en = {
show: 'Show ${1}',
hide: 'Hide ${1}',
available_items: 'Available items',
available_scopes: 'Available scopes',
required_scopes: 'Required scopes',
possible_values: 'Possible values',
properties: 'Properties',
or: 'or',
@@ -36,7 +36,7 @@ export const es = {
show: 'Mostrar ${1}',
hide: 'Ocultar ${1}',
available_items: 'Elementos disponibles',
available_scopes: 'Scopes disponibles',
required_scopes: 'Scopes requeridos',
properties: 'Propiedades',
or: 'o',
and: 'y',
@@ -36,7 +36,7 @@ export const fr = {
show: 'Afficher ${1}',
hide: 'Masquer ${1}',
available_items: 'Éléments disponibles',
available_scopes: 'Scopes disponibles',
required_scopes: 'Scopes requis',
properties: 'Propriétés',
or: 'ou',
and: 'et',
@@ -36,7 +36,7 @@ export const ja = {
show: '${1}を表示',
hide: '${1}を非表示',
available_items: '利用可能なアイテム',
available_scopes: '利用可能なスコープ',
required_scopes: '必須スコープ',
properties: 'プロパティ',
or: 'または',
and: 'かつ',
@@ -36,7 +36,7 @@ export const nl = {
show: 'Toon ${1}',
hide: 'Verberg ${1}',
available_items: 'Beschikbare items',
available_scopes: 'Beschikbare scopes',
required_scopes: 'Vereiste scopes',
properties: 'Eigenschappen',
or: 'of',
and: 'en',
@@ -36,7 +36,7 @@ export const no = {
show: 'Vis ${1}',
hide: 'Skjul ${1}',
available_items: 'Tilgjengelige elementer',
available_scopes: 'Tilgjengelige scopes',
required_scopes: 'Påkrevde scopes',
properties: 'Egenskaper',
or: 'eller',
and: 'og',
@@ -36,7 +36,7 @@ export const pt_br = {
show: 'Mostrar ${1}',
hide: 'Ocultar ${1}',
available_items: 'Itens disponíveis',
available_scopes: 'Scopes disponíveis',
required_scopes: 'Scopes obrigatórios',
properties: 'Propriedades',
or: 'ou',
and: 'e',
@@ -36,7 +36,7 @@ export const zh = {
show: '显示${1}',
hide: '隐藏${1}',
available_items: '可用项',
available_scopes: '可用范围',
required_scopes: '必需范围',
properties: '属性',
or: '或',
and: '和',
+8 -3
View File
@@ -17,8 +17,13 @@ export type OpenAPIServerWithCustomProperties = Omit<OpenAPIV3.ServerObject, 'va
};
} & OpenAPICustomPrefillProperties;
export type OpenAPISecuritySchemeWithRequired = OpenAPIV3.SecuritySchemeObject &
OpenAPICustomPrefillProperties & { required?: boolean };
export type OpenAPISecurityScope = [string, string | undefined];
export type OpenAPICustomSecurityScheme = OpenAPIV3.SecuritySchemeObject &
OpenAPICustomPrefillProperties & {
required?: boolean;
scopes?: OpenAPISecurityScope[] | null;
};
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
path: string;
@@ -31,7 +36,7 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>;
/** Securities that should be used for this operation */
securities: [string, OpenAPISecuritySchemeWithRequired][];
securities: [string, OpenAPICustomSecurityScheme][];
}
export interface OpenAPIWebhookData extends OpenAPICustomSpecProperties {
@@ -3,8 +3,8 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import type { ApiClientConfiguration } from '@scalar/types';
import type { PrefillInputContextData } from '../OpenAPIPrefillContextProvider';
import type {
OpenAPICustomSecurityScheme,
OpenAPIOperationData,
OpenAPISecuritySchemeWithRequired,
OpenAPIServerWithCustomProperties,
} from '../types';
@@ -170,7 +170,7 @@ function resolveTryItPrefillServersForOperationServers(args: {
* Return a X-GITBOOK-PREFILL placeholder based on the prefill custom property in the provided security scheme.
*/
export function resolvePrefillCodePlaceholderFromSecurityScheme(args: {
security: OpenAPISecuritySchemeWithRequired;
security: OpenAPICustomSecurityScheme;
defaultPlaceholderValue?: string;
}) {
const { security, defaultPlaceholderValue } = args;
@@ -185,7 +185,7 @@ export function resolvePrefillCodePlaceholderFromSecurityScheme(args: {
}
function extractPrefillExpressionPartsFromSecurityScheme(
security: OpenAPISecuritySchemeWithRequired
security: OpenAPICustomSecurityScheme
): TemplatePart[] {
const expression = security[PREFILL_CUSTOM_PROPERTY];
+3 -3
View File
@@ -2,7 +2,7 @@ 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 { OpenAPIOperationData, OpenAPISecuritySchemeWithRequired } from './types';
import type { OpenAPICustomSecurityScheme, OpenAPIOperationData } from './types';
export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject {
return typeof input === 'object' && !!input && '$ref' in input;
@@ -258,7 +258,7 @@ export function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
export type OperationSecurityInfo = {
key: string;
label: string;
schemes: OpenAPISecuritySchemeWithRequired[];
schemes: OpenAPICustomSecurityScheme[];
};
/**
@@ -288,7 +288,7 @@ export function extractOperationSecurityInfo(args: {
label: schemeKeys.join(' & '),
schemes: schemeKeys
.map((schemeKey) => securitiesMap.get(schemeKey))
.filter((s) => s !== undefined),
.filter((s): s is OpenAPICustomSecurityScheme => s !== undefined),
};
});
}