Skip to content

Commit eea8f1e

Browse files
authored
Enhance OpenAPI security scopes handling (#3712)
1 parent 4f9abfb commit eea8f1e

File tree

16 files changed

+154
-67
lines changed

16 files changed

+154
-67
lines changed

.changeset/clever-ducks-double.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@gitbook/react-openapi': patch
3+
'gitbook': patch
4+
---
5+
6+
Enhance OpenAPI security scopes handling

packages/gitbook/src/components/DocumentView/OpenAPI/style.css

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,18 +317,19 @@
317317
}
318318

319319
.openapi-securities-oauth-flows {
320-
@apply flex flex-col gap-2 divide-y divide-tint-subtle;
320+
@apply flex flex-col gap-3;
321321
}
322322

323-
.openapi-securities-oauth-content {
323+
.openapi-securities-oauth-content,
324+
.openapi-securities-scopes {
324325
@apply prose *:!prose-sm *:text-tint;
325326
}
326327

327328
.openapi-securities-oauth-content.openapi-markdown code {
328329
@apply text-xs;
329330
}
330331

331-
.openapi-securities-oauth-content ul {
332+
.openapi-securities-scopes ul {
332333
@apply !my-0;
333334
}
334335

packages/react-openapi/src/OpenAPISecurities.tsx

Lines changed: 76 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
2+
import { Fragment } from 'react';
23
import { InteractiveSection } from './InteractiveSection';
34
import { Markdown } from './Markdown';
45
import { OpenAPICopyButton } from './OpenAPICopyButton';
56
import { OpenAPISchemaName } from './OpenAPISchemaName';
67
import type { OpenAPIClientContext } from './context';
78
import { t } from './translate';
8-
import type { OpenAPISecuritySchemeWithRequired } from './types';
9+
import type { OpenAPICustomSecurityScheme, OpenAPISecurityScope } from './types';
910
import type { OpenAPIOperationData } from './types';
1011
import { createStateKey, extractOperationSecurityInfo, resolveDescription } from './utils';
1112

@@ -53,6 +54,12 @@ export function OpenAPISecurities(props: {
5354
className="openapi-securities-description"
5455
/>
5556
) : null}
57+
{security.scopes?.length ? (
58+
<OpenAPISchemaScopes
59+
scopes={security.scopes}
60+
context={context}
61+
/>
62+
) : null}
5663
</div>
5764
);
5865
})}
@@ -63,10 +70,7 @@ export function OpenAPISecurities(props: {
6370
);
6471
}
6572

66-
function getLabelForType(
67-
security: OpenAPISecuritySchemeWithRequired,
68-
context: OpenAPIClientContext
69-
) {
73+
function getLabelForType(security: OpenAPICustomSecurityScheme, context: OpenAPIClientContext) {
7074
switch (security.type) {
7175
case 'apiKey':
7276
return (
@@ -90,7 +94,6 @@ function getLabelForType(
9094
}
9195

9296
if (security.scheme === 'bearer') {
93-
const description = resolveDescription(security);
9497
return (
9598
<>
9699
<OpenAPISchemaName
@@ -100,7 +103,7 @@ function getLabelForType(
100103
required={security.required}
101104
/>
102105
{/** Show a default description if none is provided */}
103-
{!description ? (
106+
{!security.description ? (
104107
<Markdown
105108
source={`Bearer authentication header of the form Bearer ${'&lt;token&gt;'}.`}
106109
className="openapi-securities-description"
@@ -139,18 +142,20 @@ function OpenAPISchemaOAuth2Flows(props: {
139142
}) {
140143
const { context, security } = props;
141144

142-
const flows = Object.entries(security.flows ?? {});
145+
const flows = security.flows ? Object.entries(security.flows) : [];
143146

144147
return (
145148
<div className="openapi-securities-oauth-flows">
146149
{flows.map(([name, flow], index) => (
147-
<OpenAPISchemaOAuth2Item
148-
key={index}
149-
flow={flow}
150-
name={name}
151-
context={context}
152-
security={security}
153-
/>
150+
<Fragment key={index}>
151+
<OpenAPISchemaOAuth2Item
152+
flow={flow}
153+
name={name}
154+
context={context}
155+
security={security}
156+
/>
157+
{index < flows.length - 1 ? <hr /> : null}
158+
</Fragment>
154159
))}
155160
</div>
156161
);
@@ -170,7 +175,7 @@ function OpenAPISchemaOAuth2Item(props: {
170175
return null;
171176
}
172177

173-
const scopes = Object.entries(flow?.scopes ?? {});
178+
const scopes = flow.scopes ? Object.entries(flow.scopes) : [];
174179

175180
return (
176181
<div>
@@ -221,22 +226,62 @@ function OpenAPISchemaOAuth2Item(props: {
221226
</OpenAPICopyButton>
222227
</span>
223228
) : null}
224-
{scopes.length ? (
225-
<div>
226-
{t(context.translation, 'available_scopes')}:{' '}
227-
<ul>
228-
{scopes.map(([key, value]) => (
229-
<li key={key}>
230-
<OpenAPICopyButton value={key} context={context} withTooltip>
231-
<code>{key}</code>
232-
</OpenAPICopyButton>
233-
: {value}
234-
</li>
235-
))}
236-
</ul>
237-
</div>
238-
) : null}
229+
{scopes.length ? <OpenAPISchemaScopes scopes={scopes} context={context} /> : null}
239230
</div>
240231
</div>
241232
);
242233
}
234+
235+
/**
236+
* Render a list of available scopes.
237+
*/
238+
function OpenAPISchemaScopes(props: {
239+
scopes: OpenAPISecurityScope[];
240+
context: OpenAPIClientContext;
241+
}) {
242+
const { scopes, context } = props;
243+
244+
return (
245+
<div className="openapi-securities-scopes openapi-markdown">
246+
<span>{t(context.translation, 'required_scopes')}: </span>
247+
<ul>
248+
{scopes.map((scope) => (
249+
<OpenAPIScopeItem key={scope[0]} scope={scope} context={context} />
250+
))}
251+
</ul>
252+
</div>
253+
);
254+
}
255+
256+
/**
257+
* Display a scope item. Either a key-value pair or a single string.
258+
*/
259+
function OpenAPIScopeItem(props: {
260+
scope: OpenAPISecurityScope;
261+
context: OpenAPIClientContext;
262+
}) {
263+
const { scope, context } = props;
264+
265+
return (
266+
<li>
267+
<OpenAPIScopeItemKey name={scope[0]} context={context} />
268+
{scope[1] ? `: ${scope[1]}` : null}
269+
</li>
270+
);
271+
}
272+
273+
/**
274+
* Displays the scope name within a copyable button.
275+
*/
276+
function OpenAPIScopeItemKey(props: {
277+
name: string;
278+
context: OpenAPIClientContext;
279+
}) {
280+
const { name, context } = props;
281+
282+
return (
283+
<OpenAPICopyButton value={name} context={context} withTooltip>
284+
<code>{name}</code>
285+
</OpenAPICopyButton>
286+
);
287+
}

packages/react-openapi/src/resolveOpenAPIOperation.ts

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
OpenAPIV3xDocument,
88
} from '@gitbook/openapi-parser';
99
import { dereferenceFilesystem } from './dereference';
10-
import type { OpenAPIOperationData } from './types';
10+
import type { OpenAPIOperationData, OpenAPISecurityScope } from './types';
1111
import { checkIsReference } from './utils';
1212

1313
export { fromJSON, toJSON };
@@ -54,18 +54,21 @@ export async function resolveOpenAPIOperation(
5454
// Resolve securities
5555
const securities: OpenAPIOperationData['securities'] = [];
5656
for (const entry of flatSecurities) {
57-
const securityKey = Object.keys(entry)[0];
57+
const [securityKey, operationScopes] = Object.entries(entry)[0] ?? [];
5858
if (securityKey) {
5959
const securityScheme = schema.components?.securitySchemes?.[securityKey];
60-
if (securityScheme && !checkIsReference(securityScheme)) {
61-
securities.push([
62-
securityKey,
63-
{
64-
...securityScheme,
65-
required: !isOptionalSecurity,
66-
},
67-
]);
68-
}
60+
const scopes = resolveSecurityScopes({
61+
securityScheme,
62+
operationScopes,
63+
});
64+
securities.push([
65+
securityKey,
66+
{
67+
...securityScheme,
68+
required: !isOptionalSecurity,
69+
scopes,
70+
},
71+
]);
6972
}
7073
}
7174

@@ -91,10 +94,7 @@ function getPathObject(
9194
schema: OpenAPIV3.Document | OpenAPIV3_1.Document,
9295
path: string
9396
): OpenAPIV3.PathItemObject | OpenAPIV3_1.PathItemObject | null {
94-
if (schema.paths?.[path]) {
95-
return schema.paths[path];
96-
}
97-
return null;
97+
return schema.paths?.[path] || null;
9898
}
9999

100100
/**
@@ -149,3 +149,33 @@ function flattenSecurities(security: OpenAPIV3.SecurityRequirementObject[]) {
149149
}));
150150
});
151151
}
152+
153+
/**
154+
* Resolve the scopes for a security scheme.
155+
*/
156+
function resolveSecurityScopes({
157+
securityScheme,
158+
operationScopes,
159+
}: {
160+
securityScheme?: OpenAPIV3.ReferenceObject | OpenAPIV3.SecuritySchemeObject;
161+
operationScopes?: string[];
162+
}): OpenAPISecurityScope[] | null {
163+
if (
164+
!securityScheme ||
165+
checkIsReference(securityScheme) ||
166+
isOAuthSecurityScheme(securityScheme)
167+
) {
168+
return null;
169+
}
170+
171+
return operationScopes?.map((scope) => [scope, undefined]) || [];
172+
}
173+
174+
/**
175+
* Check if a security scheme is an OAuth or OpenID Connect security scheme.
176+
*/
177+
function isOAuthSecurityScheme(
178+
securityScheme: OpenAPIV3.SecuritySchemeObject
179+
): securityScheme is OpenAPIV3.OAuth2SecurityScheme {
180+
return securityScheme.type === 'oauth2';
181+
}

packages/react-openapi/src/translations/de.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const de = {
3636
show: 'Zeige ${1}',
3737
hide: 'Verstecke ${1}',
3838
available_items: 'Verfügbare Elemente',
39-
available_scopes: 'Verfügbare scopes',
39+
required_scopes: 'Erforderliche Scopes',
4040
properties: 'Eigenschaften',
4141
or: 'oder',
4242
and: 'und',

packages/react-openapi/src/translations/en.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const en = {
3636
show: 'Show ${1}',
3737
hide: 'Hide ${1}',
3838
available_items: 'Available items',
39-
available_scopes: 'Available scopes',
39+
required_scopes: 'Required scopes',
4040
possible_values: 'Possible values',
4141
properties: 'Properties',
4242
or: 'or',

packages/react-openapi/src/translations/es.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const es = {
3636
show: 'Mostrar ${1}',
3737
hide: 'Ocultar ${1}',
3838
available_items: 'Elementos disponibles',
39-
available_scopes: 'Scopes disponibles',
39+
required_scopes: 'Scopes requeridos',
4040
properties: 'Propiedades',
4141
or: 'o',
4242
and: 'y',

packages/react-openapi/src/translations/fr.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const fr = {
3636
show: 'Afficher ${1}',
3737
hide: 'Masquer ${1}',
3838
available_items: 'Éléments disponibles',
39-
available_scopes: 'Scopes disponibles',
39+
required_scopes: 'Scopes requis',
4040
properties: 'Propriétés',
4141
or: 'ou',
4242
and: 'et',

packages/react-openapi/src/translations/ja.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const ja = {
3636
show: '${1}を表示',
3737
hide: '${1}を非表示',
3838
available_items: '利用可能なアイテム',
39-
available_scopes: '利用可能なスコープ',
39+
required_scopes: '必須スコープ',
4040
properties: 'プロパティ',
4141
or: 'または',
4242
and: 'かつ',

packages/react-openapi/src/translations/nl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const nl = {
3636
show: 'Toon ${1}',
3737
hide: 'Verberg ${1}',
3838
available_items: 'Beschikbare items',
39-
available_scopes: 'Beschikbare scopes',
39+
required_scopes: 'Vereiste scopes',
4040
properties: 'Eigenschappen',
4141
or: 'of',
4242
and: 'en',

0 commit comments

Comments
 (0)