mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Enhance OpenAPI security scopes handling (#3712)
This commit is contained in:
@@ -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 ${'<token>'}.`}
|
||||
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: '和',
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user