Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/clever-ducks-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@gitbook/react-openapi': patch
'gitbook': patch
---

Enhance OpenAPI security scopes handling
Original file line number Diff line number Diff line change
Expand Up @@ -317,18 +317,19 @@
}

.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;
}

.openapi-securities-oauth-content.openapi-markdown code {
@apply text-xs;
}

.openapi-securities-oauth-content ul {
.openapi-securities-scopes ul {
@apply !my-0;
}

Expand Down
107 changes: 76 additions & 31 deletions packages/react-openapi/src/OpenAPISecurities.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -53,6 +54,12 @@ export function OpenAPISecurities(props: {
className="openapi-securities-description"
/>
) : null}
{security.scopes?.length ? (
<OpenAPISchemaScopes
scopes={security.scopes}
context={context}
/>
) : null}
</div>
);
})}
Expand All @@ -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 (
Expand All @@ -90,7 +94,6 @@ function getLabelForType(
}

if (security.scheme === 'bearer') {
const description = resolveDescription(security);
return (
<>
<OpenAPISchemaName
Expand All @@ -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"
Expand Down Expand Up @@ -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>
);
Expand All @@ -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>
Expand Down Expand Up @@ -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>
);
}
60 changes: 45 additions & 15 deletions packages/react-openapi/src/resolveOpenAPIOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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,
},
]);
}
}

Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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';
}
2 changes: 1 addition & 1 deletion packages/react-openapi/src/translations/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/react-openapi/src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/react-openapi/src/translations/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/react-openapi/src/translations/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/react-openapi/src/translations/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const ja = {
show: '${1}を表示',
hide: '${1}を非表示',
available_items: '利用可能なアイテム',
available_scopes: '利用可能なスコープ',
required_scopes: '必須スコープ',
properties: 'プロパティ',
or: 'または',
and: 'かつ',
Expand Down
2 changes: 1 addition & 1 deletion packages/react-openapi/src/translations/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/react-openapi/src/translations/no.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/react-openapi/src/translations/pt-br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/react-openapi/src/translations/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const zh = {
show: '显示${1}',
hide: '隐藏${1}',
available_items: '可用项',
available_scopes: '可用范围',
required_scopes: '必需范围',
properties: '属性',
or: '或',
and: '和',
Expand Down
11 changes: 8 additions & 3 deletions packages/react-openapi/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
Loading