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