forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Security Solution] Implement shared components conflict resolution f…
…unctionality (elastic#188812) **Resolves:** elastic#188817 This PR adds automatic shared components conflict resolution functionality for OpenAPI merger. It boils down to a similar result as `npx @redocly/cli join --prefix-components-with-info-prop title` produces by prefixing shared components with document's title in each source. OpenAPI bundler intentionally won't solve conflicts automatically since it's focused on bundling domain APIs where conflicts are usually indicators of upstream problems. While working with various OpenAPI specs it may happen that different specs use exactly the same name for some shared components but different definitions. It must be avoided inside one API domain but it's a usual situation when merging OpenAPI specs of different API domains. For example domains may define a shared `Id` or `404Response` schemas where `Id` is a string in one domain and a number in another. OpenAPI merger implemented in elastic#188110 and OpenAPI bundler implemented in elastic#171526 do not solve shared components related conflicts automatically. It works perfectly for a single API domain forcing engineers choosing shared schema names carefully. This PR adds automatic shared components conflict resolution for OpenAPI merger. It prefixes shared component names with a normalized document's title. OpenAPI bundler intentionally won't solve conflicts automatically since it's focused on bundling domain APIs where conflicts are usually indicators of upstream problems. Consider two following OpenAPI specs each defining local `MySchema` **spec1.schema.yaml** ```yaml openapi: 3.0.3 info: title: My endpoint version: '2023-10-31' paths: /api/some_api: get: operationId: MyEndpointGet responses: '200': content: application/json: schema: $ref: '#/components/schemas/MySchema' components: schemas: MySchema: type: string enum: - value1 ``` **spec2.schema.yaml** ```yaml openapi: 3.0.3 info: title: My another endpoint version: '2023-10-31' paths: /api/another_api: get: operationId: MyAnotherEndpointGet responses: '200': content: application/json: schema: $ref: '#/components/schemas/MySchema' components: schemas: MySchema: type: number ``` and a script to merge them ```js require('../../src/setup_node_env'); const { resolve } = require('path'); const { merge } = require('@kbn/openapi-bundler'); const { REPO_ROOT } = require('@kbn/repo-info'); (async () => { await merge({ sourceGlobs: [ `${REPO_ROOT}/oas_docs/spec1.schema.yaml`, `${REPO_ROOT}/oas_docs/spec2.schema.yaml`, ], outputFilePath: resolve(`${REPO_ROOT}/oas_docs/merged.yaml`), options: { mergedSpecInfo: { title: 'Merge result', version: 'my version', }, }, }); })(); ``` will be merged successfully to **merged.yaml** ```yaml openapi: 3.0.3 info: title: Merge result version: 'my version' paths: /api/another_api: get: operationId: MyAnotherEndpointGet responses: '200': content: application/json; Elastic-Api-Version=2023-10-31: schema: $ref: '#/components/schemas/My_another_endpoint_MySchema' /api/some_api: get: operationId: MyEndpointGet responses: '200': content: application/json; Elastic-Api-Version=2023-10-31: schema: $ref: '#/components/schemas/My_endpoint_MySchema' components: schemas: My_another_endpoint_MySchema: type: number My_endpoint_MySchema: enum: - value1 type: string ```
- Loading branch information
Showing
23 changed files
with
1,323 additions
and
221 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
148 changes: 148 additions & 0 deletions
148
...api-bundler/src/bundler/process_document/document_processors/namespace_components.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { createNamespaceComponentsProcessor } from './namespace_components'; | ||
|
||
describe('namespaceComponentsProcessor', () => { | ||
it.each([ | ||
{ | ||
sourceValue: 'Something', | ||
originalRef: '#/components/schemas/SomeComponent', | ||
expectedRef: '#/components/schemas/Something_SomeComponent', | ||
}, | ||
{ | ||
sourceValue: 'Some Domain API (Extra Information)', | ||
originalRef: '#/components/schemas/SomeComponent', | ||
expectedRef: '#/components/schemas/Some_Domain_API_SomeComponent', | ||
}, | ||
{ | ||
sourceValue: 'Hello, world!', | ||
originalRef: '#/components/schemas/SomeComponent', | ||
expectedRef: '#/components/schemas/Hello_world_SomeComponent', | ||
}, | ||
{ | ||
sourceValue: 'Something', | ||
originalRef: '../path/to/some.schema.yaml#/components/schemas/SomeComponent', | ||
expectedRef: '../path/to/some.schema.yaml#/components/schemas/Something_SomeComponent', | ||
}, | ||
{ | ||
sourceValue: 'Some Domain API (Extra Information)', | ||
originalRef: '../path/to/some.schema.yaml#/components/schemas/SomeComponent', | ||
expectedRef: '../path/to/some.schema.yaml#/components/schemas/Some_Domain_API_SomeComponent', | ||
}, | ||
{ | ||
sourceValue: 'Hello, world!', | ||
originalRef: '../path/to/some.schema.yaml#/components/schemas/SomeComponent', | ||
expectedRef: '../path/to/some.schema.yaml#/components/schemas/Hello_world_SomeComponent', | ||
}, | ||
])( | ||
'prefixes reference "$originalRef" with normalized "$sourceValue"', | ||
({ sourceValue, originalRef, expectedRef }) => { | ||
const processor = createNamespaceComponentsProcessor('/info/title'); | ||
|
||
const document = { | ||
info: { | ||
title: sourceValue, | ||
}, | ||
}; | ||
|
||
processor.onNodeEnter?.(document, { | ||
resolvedDocument: { absolutePath: '', document }, | ||
isRootNode: true, | ||
parentNode: document, | ||
parentKey: '', | ||
}); | ||
|
||
const node = { $ref: originalRef }; | ||
|
||
processor.onRefNodeLeave?.( | ||
node, | ||
{ pointer: '', refNode: {}, absolutePath: '', document: {} }, | ||
{ | ||
resolvedDocument: { absolutePath: '', document }, | ||
isRootNode: false, | ||
parentNode: document, | ||
parentKey: '', | ||
} | ||
); | ||
|
||
expect(node).toMatchObject({ | ||
$ref: expectedRef, | ||
}); | ||
} | ||
); | ||
|
||
it('prefixes security requirements', () => { | ||
const processor = createNamespaceComponentsProcessor('/info/title'); | ||
|
||
const document = { | ||
info: { | ||
title: 'Something', | ||
}, | ||
}; | ||
|
||
processor.onNodeEnter?.(document, { | ||
resolvedDocument: { absolutePath: '', document }, | ||
isRootNode: true, | ||
parentNode: document, | ||
parentKey: '', | ||
}); | ||
|
||
const node = { security: [{ SomeSecurityRequirement: [] }] }; | ||
|
||
processor.onNodeLeave?.(node, { | ||
resolvedDocument: { absolutePath: '', document }, | ||
isRootNode: false, | ||
parentNode: document, | ||
parentKey: '', | ||
}); | ||
|
||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
expect(node).toMatchObject({ security: [{ Something_SomeSecurityRequirement: [] }] }); | ||
}); | ||
|
||
it('prefixes security requirement components', () => { | ||
const processor = createNamespaceComponentsProcessor('/info/title'); | ||
|
||
const document = { | ||
info: { | ||
title: 'Something', | ||
}, | ||
components: { | ||
securitySchemes: { | ||
BasicAuth: { | ||
scheme: 'basic', | ||
type: 'http', | ||
}, | ||
}, | ||
}, | ||
}; | ||
|
||
processor.onNodeEnter?.(document, { | ||
resolvedDocument: { absolutePath: '', document }, | ||
isRootNode: true, | ||
parentNode: document, | ||
parentKey: '', | ||
}); | ||
|
||
processor.onNodeLeave?.(document, { | ||
resolvedDocument: { absolutePath: '', document }, | ||
isRootNode: true, | ||
parentNode: document, | ||
parentKey: '', | ||
}); | ||
|
||
expect(document.components.securitySchemes).toMatchObject({ | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
Something_BasicAuth: { | ||
scheme: 'basic', | ||
type: 'http', | ||
}, | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.