-
Notifications
You must be signed in to change notification settings - Fork 217
fix: resolve root security during join #2827
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
be50fd4
04c85ee
ecc35f1
087e650
8f481fa
162da39
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@redocly/cli': patch | ||
| --- | ||
|
|
||
| Fixed the `join` command to apply root-level `security` from each joined API to operations that do not define their own `security`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,7 +26,7 @@ import { | |
| writeToFileByExtension, | ||
| } from '../../utils/miscellaneous.js'; | ||
| import type { CommandArgs } from '../../wrapper.js'; | ||
| import { COMPONENTS } from '../split/constants.js'; | ||
| // import { COMPONENTS } from '../split/constants.js'; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove comments. |
||
| import type { JoinArgv, AnyOas3Definition } from './types.js'; | ||
| import { | ||
| replace$Refs, | ||
|
|
@@ -39,6 +39,8 @@ import { | |
| collectComponents, | ||
| collectWebhooks, | ||
| addInfoSectionAndSpecVersion, | ||
| collectDuplicateSecuritySchemeNames, | ||
| getEffectiveComponentsPrefix, | ||
| } from './utils/index.js'; | ||
|
|
||
| export async function handleJoin({ | ||
|
|
@@ -174,6 +176,10 @@ export async function handleJoin({ | |
| joinedDef.servers = first.parsed.servers; | ||
| } | ||
|
|
||
| const duplicateSecuritySchemeNames = collectDuplicateSecuritySchemeNames( | ||
| documents as Document<AnyOas3Definition>[] | ||
| ); | ||
|
|
||
| for (const document of documents) { | ||
| const openapi = isPlainObject<AnyOas3Definition>(document.parsed) | ||
| ? document.parsed | ||
|
|
@@ -184,7 +190,12 @@ export async function handleJoin({ | |
| const tagsPrefix = prefixTagsWithFilename | ||
| ? apiFilename | ||
| : getInfoPrefix(info, prefixTagsWithInfoProp, 'tags'); | ||
| const componentsPrefix = getInfoPrefix(info, prefixComponentsWithInfoProp, COMPONENTS); | ||
| const componentsPrefix = getEffectiveComponentsPrefix({ | ||
| info, | ||
| apiFilename, | ||
| prefixComponentsWithInfoProp, | ||
| duplicateSecuritySchemeNames, | ||
| }); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Info description prefix diverges from main component prefixLow Severity
Reviewed by Cursor Bugbot for commit 162da39. Configure here. |
||
|
|
||
| if (openapi.hasOwnProperty('x-tagGroups')) { | ||
| logger.warn(`warning: x-tagGroups at ${blue(api)} will be skipped \n`); | ||
|
|
@@ -203,6 +214,7 @@ export async function handleJoin({ | |
| if (tags) { | ||
| populateTags({ joinedDef, withoutXTagGroups, context }); | ||
| } | ||
|
|
||
| collectExternalDocs({ joinedDef, openapi, context }); | ||
| collectPaths({ joinedDef, withoutXTagGroups, openapi, context, serversAreTheSame }); | ||
| collectComponents({ joinedDef, openapi, context }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,9 +14,9 @@ import { OPENAPI3_METHOD_NAMES } from '../../split/oas/constants.js'; | |
| import { type Oas3Method } from '../../split/types.js'; | ||
| import type { AnyOas3Definition, JoinDocumentContext } from '../types.js'; | ||
| import { addPrefix } from './add-prefix.js'; | ||
| import { addSecurityPrefix } from './add-security-prefix.js'; | ||
| import { formatTags } from './format-tags.js'; | ||
| import { populateTags } from './populate-tags.js'; | ||
| import { resolveOperationSecurity } from './resolve-operation-security.js'; | ||
|
|
||
| export function collectPaths({ | ||
| joinedDef, | ||
|
|
@@ -181,7 +181,7 @@ export function collectPaths({ | |
| ]; | ||
| } | ||
|
|
||
| const { tags, security } = joinedDef.paths[path][operation]; | ||
| const { tags } = joinedDef.paths[path][operation]; | ||
|
|
||
| if (tags) { | ||
| joinedDef.paths[path][operation].tags = tags.map((tag: string) => addPrefix(tag, tagsPrefix)); | ||
|
|
@@ -216,16 +216,15 @@ export function collectPaths({ | |
| }, | ||
| }); | ||
| } | ||
| if (!security && openapi.hasOwnProperty('security')) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the previous solution was not working? I see it refers to the root security already. |
||
| joinedDef.paths[path][operation]['security'] = addSecurityPrefix( | ||
| openapi.security, | ||
| componentsPrefix! | ||
| ); | ||
| } else if (pathOperation.security) { | ||
| joinedDef.paths[path][operation].security = addSecurityPrefix( | ||
| pathOperation.security, | ||
| componentsPrefix! | ||
| ); | ||
| const operationSecurity = resolveOperationSecurity({ | ||
| pathItem, | ||
| pathOperation, | ||
| openapi, | ||
| componentsPrefix, | ||
| }); | ||
|
|
||
| if (operationSecurity) { | ||
| joinedDef.paths[path][operation].security = operationSecurity; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import { type Document, type Oas3PathItem, isPlainObject, keysOf } from '@redocly/openapi-core'; | ||
|
|
||
| import { COMPONENTS } from '../../split/constants.js'; | ||
| import { OPENAPI3_METHOD_NAMES } from '../../split/oas/constants.js'; | ||
| import { type Oas3Method } from '../../split/types.js'; | ||
| import type { AnyOas3Definition } from '../types.js'; | ||
| import { getInfoPrefix } from './get-info-prefix.js'; | ||
|
|
||
| const operationsSet = new Set(OPENAPI3_METHOD_NAMES); | ||
|
|
||
| function collectSecuritySchemeNamesFromApi(openapi: AnyOas3Definition): Set<string> { | ||
| const names = new Set<string>(); | ||
|
|
||
| const addFromSecurity = (security: AnyOas3Definition['security']) => { | ||
| if (!security) { | ||
| return; | ||
| } | ||
| for (const requirement of security) { | ||
| for (const schemeName of Object.keys(requirement)) { | ||
| names.add(schemeName); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| addFromSecurity(openapi.security); | ||
|
|
||
| for (const pathItem of Object.values(openapi.paths ?? {})) { | ||
| if (!pathItem || !isPlainObject(pathItem)) { | ||
| continue; | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| const pathItemObject = pathItem as Oas3PathItem; | ||
|
|
||
| addFromSecurity( | ||
| (pathItemObject as Oas3PathItem & { security?: AnyOas3Definition['security'] }).security | ||
| ); | ||
|
|
||
| for (const field of keysOf(pathItemObject)) { | ||
| if (operationsSet.has(field as Oas3Method)) { | ||
| addFromSecurity(pathItemObject[field as Oas3Method]?.security); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| for (const schemeName of Object.keys(openapi.components?.securitySchemes ?? {})) { | ||
| names.add(schemeName); | ||
| } | ||
|
|
||
| return names; | ||
| } | ||
|
|
||
| export function collectDuplicateSecuritySchemeNames( | ||
| documents: Document<AnyOas3Definition>[] | ||
| ): Set<string> { | ||
| const apisPerScheme = new Map<string, number>(); | ||
|
|
||
| for (const document of documents) { | ||
| const schemeNames = collectSecuritySchemeNamesFromApi(document.parsed); | ||
|
|
||
| for (const name of schemeNames) { | ||
| apisPerScheme.set(name, (apisPerScheme.get(name) ?? 0) + 1); | ||
| } | ||
| } | ||
|
|
||
| return new Set( | ||
| [...apisPerScheme.entries()].filter(([, apiCount]) => apiCount > 1).map(([name]) => name) | ||
| ); | ||
| } | ||
|
|
||
| export function getEffectiveComponentsPrefix({ | ||
| info, | ||
| apiFilename, | ||
| prefixComponentsWithInfoProp, | ||
| duplicateSecuritySchemeNames, | ||
| }: { | ||
| info: AnyOas3Definition['info']; | ||
| apiFilename: string; | ||
| prefixComponentsWithInfoProp?: string; | ||
| duplicateSecuritySchemeNames: Set<string>; | ||
| }) { | ||
| const explicitPrefix = getInfoPrefix(info, prefixComponentsWithInfoProp, COMPONENTS); | ||
| if (explicitPrefix) { | ||
| return explicitPrefix; | ||
| } | ||
|
|
||
| if (!duplicateSecuritySchemeNames.size) { | ||
| return ''; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Auto-prefixing applies to all APIs, not just conflicting onesMedium Severity
Reviewed by Cursor Bugbot for commit 162da39. Configure here. |
||
|
|
||
| const raw = info?.title ?? apiFilename; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nullish coalescing fails for empty title stringLow Severity
Reviewed by Cursor Bugbot for commit 5bc9605. Configure here. |
||
| return raw.replaceAll(/\s/g, '_'); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import type { Oas3PathItem } from '@redocly/openapi-core'; | ||
|
|
||
| import { type Oas3Method } from '../../split/types.js'; | ||
| import type { AnyOas3Definition } from '../types.js'; | ||
| import { addSecurityPrefix } from './add-security-prefix.js'; | ||
|
|
||
| export function resolveOperationSecurity({ | ||
| pathItem, | ||
| pathOperation, | ||
| openapi, | ||
| componentsPrefix, | ||
| }: { | ||
| pathItem: Oas3PathItem; | ||
| pathOperation: NonNullable<Oas3PathItem[Oas3Method]>; | ||
| openapi: AnyOas3Definition; | ||
| componentsPrefix: string | undefined; | ||
| }) { | ||
| if (pathOperation.hasOwnProperty('security')) { | ||
| return addSecurityPrefix(pathOperation.security, componentsPrefix!); | ||
| } | ||
|
|
||
| if (pathItem.hasOwnProperty('security')) { | ||
| return addSecurityPrefix( | ||
| (pathItem as Oas3PathItem & { security: AnyOas3Definition['security'] }).security, | ||
| componentsPrefix! | ||
| ); | ||
| } | ||
|
|
||
| if (openapi.hasOwnProperty('security')) { | ||
| return addSecurityPrefix(openapi.security, componentsPrefix!); | ||
| } | ||
|
|
||
| return undefined; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| openapi: 3.1.0 | ||
| info: | ||
| title: spec2 | ||
| version: 1.0.0 | ||
| servers: | ||
| - url: https://api.example.com | ||
| components: | ||
| securitySchemes: | ||
| oauth2: | ||
| type: oauth2 | ||
| flows: | ||
| authorizationCode: | ||
| authorizationUrl: https://example.com/oauth/authorize2 | ||
| tokenUrl: https://example.com/oauth/token2 | ||
| scopes: {} | ||
| paths: | ||
| /orders: | ||
| get: | ||
| summary: Orders from spec2 | ||
| operationId: getOrdersSpec2 | ||
| security: | ||
| - oauth2: [] | ||
| responses: | ||
| '200': | ||
| description: OK |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| openapi: 3.1.0 | ||
| info: | ||
| title: spec1 | ||
| version: 1.0.0 | ||
| servers: | ||
| - url: https://api.example.com | ||
| components: | ||
| securitySchemes: | ||
| oauth2: | ||
| type: oauth2 | ||
| flows: | ||
| authorizationCode: | ||
| authorizationUrl: https://example.com/oauth/authorize1 | ||
| tokenUrl: https://example.com/oauth/token1 | ||
| scopes: {} | ||
| paths: | ||
| /pets: | ||
| get: | ||
| summary: Pets from spec1 | ||
| operationId: getPetsSpec1 | ||
| security: | ||
| - oauth2: [] | ||
| responses: | ||
| '200': | ||
| description: OK |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
|
|
||
| openapi: 3.1.0 | ||
| info: | ||
| title: spec1 | ||
| version: 1.0.0 | ||
| servers: | ||
| - url: https://api.example.com | ||
| tags: | ||
| - name: foo_other | ||
| x-displayName: other | ||
| - name: bar_other | ||
| x-displayName: other | ||
| paths: | ||
| /pets: | ||
| get: | ||
| summary: Pets from spec1 | ||
| operationId: getPetsSpec1 | ||
| security: | ||
| - spec1_oauth2: [] | ||
| responses: | ||
| '200': | ||
| description: OK | ||
| tags: | ||
| - foo_other | ||
| /orders: | ||
| get: | ||
| summary: Orders from spec2 | ||
| operationId: getOrdersSpec2 | ||
| security: | ||
| - spec2_oauth2: [] | ||
| responses: | ||
| '200': | ||
| description: OK | ||
| tags: | ||
| - bar_other | ||
| components: | ||
| securitySchemes: | ||
| spec1_oauth2: | ||
| type: oauth2 | ||
| flows: | ||
| authorizationCode: | ||
| authorizationUrl: https://example.com/oauth/authorize1 | ||
| tokenUrl: https://example.com/oauth/token1 | ||
| scopes: {} | ||
| spec2_oauth2: | ||
| type: oauth2 | ||
| flows: | ||
| authorizationCode: | ||
| authorizationUrl: https://example.com/oauth/authorize2 | ||
| tokenUrl: https://example.com/oauth/token2 | ||
| scopes: {} | ||
| x-tagGroups: | ||
| - name: spec1 | ||
| tags: | ||
| - foo_other | ||
| - name: spec2 | ||
| tags: | ||
| - bar_other | ||
|
|
||
| openapi.yaml: join processed in <test>ms | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| openapi: 3.1.0 | ||
| info: | ||
| title: spec2 | ||
| version: 1.0.0 | ||
| servers: | ||
| - url: https://api.example.com | ||
| security: | ||
| - oauth2: [] | ||
| components: | ||
| securitySchemes: | ||
| oauth2: | ||
| type: oauth2 | ||
| flows: | ||
| authorizationCode: | ||
| authorizationUrl: https://example.com/oauth/authorize2 | ||
| tokenUrl: https://example.com/oauth/token2 | ||
| scopes: {} | ||
| paths: | ||
| /orders: | ||
| get: | ||
| summary: Orders from spec2 | ||
| operationId: getOrdersSpec2 | ||
| responses: | ||
| '200': | ||
| description: OK |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commented-out import instead of removal
Low Severity
The
COMPONENTSimport is commented out (// import { COMPONENTS } from ...) rather than deleted. This constant is no longer used inindex.tsbecause the newgetEffectiveComponentsPrefixutility handles it internally. The commented-out line is dead code that appears to be left over from development.Reviewed by Cursor Bugbot for commit b647fd9. Configure here.