Skip to content
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

[Security Solution] Add tags by OpenAPI bundler #189621

Merged
merged 6 commits into from
Aug 2, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ export async function mergeDocuments(
mergedDocument.security = mergeSecurityRequirements(documentsToMerge);
}

mergedDocument.tags = mergeTags(documentsToMerge);
const mergedTags = [...(options.addTags ?? []), ...(mergeTags(documentsToMerge) ?? [])];

if (mergedTags.length) {
mergedDocument.tags = mergedTags;
}

mergedByVersion.set(mergedDocument.info.version, mergedDocument);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,17 @@ export function mergeOperations(
continue;
}

// Adding tags before merging helps to reuse already existing functionality
// without changes. It imitates a case when such tags already existed in source operations.
const extendedTags = [
...(options.addTags?.map((t) => t.name) ?? []),
...(sourceOperation.tags ?? []),
];
const normalizedSourceOperation = {
...sourceOperation,
...(options.skipServers ? { servers: undefined } : { servers: sourceOperation.servers }),
...(options.skipSecurity ? { security: undefined } : { security: sourceOperation.security }),
...(extendedTags.length > 0 ? { tags: extendedTags } : {}),
};

if (!mergedOperation || deepEqual(normalizedSourceOperation, mergedOperation)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
* Side Public License, v 1.
*/

import { OpenAPIV3 } from 'openapi-types';

export interface MergeOptions {
skipServers: boolean;
skipSecurity: boolean;
addTags?: OpenAPIV3.TagObject[];
}
5 changes: 3 additions & 2 deletions packages/kbn-openapi-bundler/src/openapi_bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ interface BundleOptions {
*/
prototypeDocument?: PrototypeDocument | string;
/**
* When specified the produced bundle will contain only
* When `includeLabels` are specified the produced bundle will contain only
* operations objects with matching labels
*/
includeLabels?: string[];
}

export const bundle = async ({
sourceGlob,
outputFilePath = 'bundled-{version}.schema.yaml',
outputFilePath = 'bundled_{version}.schema.yaml',
options,
}: BundlerConfig) => {
const prototypeDocument = options?.prototypeDocument
Expand Down Expand Up @@ -82,6 +82,7 @@ export const bundle = async ({
splitDocumentsByVersion: true,
skipServers: Boolean(prototypeDocument?.servers),
skipSecurity: Boolean(prototypeDocument?.security),
addTags: prototypeDocument?.tags,
});

await writeDocuments(resultDocumentsMap, outputFilePath);
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-openapi-bundler/src/openapi_merger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const merge = async ({
splitDocumentsByVersion: false,
skipServers: Boolean(prototypeDocument?.servers),
skipSecurity: Boolean(prototypeDocument?.security),
addTags: prototypeDocument?.tags,
});
// Only one document is expected when `splitDocumentsByVersion` is set to `false`
const mergedDocument = Array.from(resultDocumentsMap.values())[0];
Expand Down
34 changes: 25 additions & 9 deletions packages/kbn-openapi-bundler/src/prototype_document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,37 @@
import { OpenAPIV3 } from 'openapi-types';

/**
* `PrototypeDocument` is used as a prototype for the result file. In the other words
* it provides a way to specify the following properties
*
* - `info` info object
* - `servers` servers used to replace `servers` in the source OpenAPI specs
* - `security` security requirements used to replace `security` in the source OpenAPI specs
* It must be specified together with `components.securitySchemes`.
*
* All the other properties will be ignored.
* `PrototypeDocument` is used as a prototype for the result file.
* Only specified properties are used. All the other properties will be ignored.
*/
export interface PrototypeDocument {
/**
* Defines OpenAPI Info Object to be used in the result document.
* `bundle()` utility doesn't use `info.version`.
*/
info?: Partial<OpenAPIV3.InfoObject>;
/**
* Defines `servers` to be used in the result document. When `servers`
* are set existing source documents `servers` aren't included into
* the result document.
*/
servers?: OpenAPIV3.ServerObject[];
/**
* Defines security requirements to be used in the result document. It must
* be used together with `components.securitySchemes` When `security`
* is set existing source documents `security` isn't included into
* the result document.
*/
security?: OpenAPIV3.SecurityRequirementObject[];
components?: {
/**
* Defines security schemes for security requirements.
*/
securitySchemes: Record<string, OpenAPIV3.SecuritySchemeObject>;
};
/**
* Defines tags to be added to the result document. Tags are added to
* root level tags and prepended to operation object tags.
*/
tags?: OpenAPIV3.TagObject[];
}
28 changes: 28 additions & 0 deletions packages/kbn-openapi-bundler/src/validate_prototype_document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,34 @@ export async function validatePrototypeDocument(
? await readDocument(prototypeDocumentOrString)
: prototypeDocumentOrString;

if (prototypeDocument.servers && !Array.isArray(prototypeDocument.servers)) {
throw new Error(`Prototype document's ${chalk.bold('servers')} must be an array`);
}

if (prototypeDocument.servers && prototypeDocument.servers.length === 0) {
throw new Error(
`Prototype document's ${chalk.bold('servers')} should have as minimum one entry`
);
}

if (prototypeDocument.security && !Array.isArray(prototypeDocument.security)) {
throw new Error(`Prototype document's ${chalk.bold('security')} must be an array`);
}

if (prototypeDocument.security && prototypeDocument.security.length === 0) {
throw new Error(
`Prototype document's ${chalk.bold('security')} should have as minimum one entry`
);
}

if (prototypeDocument.tags && !Array.isArray(prototypeDocument.tags)) {
throw new Error(`Prototype document's ${chalk.bold('tags')} must be an array`);
}

if (prototypeDocument.tags && prototypeDocument.tags.length === 0) {
throw new Error(`Prototype document's ${chalk.bold('tags')} should have as minimum one entry`);
}

if (prototypeDocument.security && !prototypeDocument.components?.securitySchemes) {
throw new Error(
`Prototype document must contain ${chalk.bold(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* 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 { createOASDocument } from '../../create_oas_document';
import { bundleSpecs } from '../bundle_specs';

describe('OpenAPI Bundler - assign a tag', () => {
it('adds tags when nothing is set', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
});
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
});

const [bundledSpec] = Object.values(
await bundleSpecs(
{
1: spec1,
2: spec2,
},
{
prototypeDocument: {
tags: [
{
name: 'Some Tag',
description: 'Some tag description',
},
{
name: 'Another Tag',
description: 'Another tag description',
},
],
},
}
)
);

expect(bundledSpec.paths['/api/some_api']?.get?.tags).toEqual(['Some Tag', 'Another Tag']);
expect(bundledSpec.paths['/api/another_api']?.get?.tags).toEqual(['Some Tag', 'Another Tag']);
expect(bundledSpec.tags).toEqual([
{
name: 'Some Tag',
description: 'Some tag description',
},
{
name: 'Another Tag',
description: 'Another tag description',
},
]);
});

it('adds tags to existing tags', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
tags: ['Local tag'],
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
});
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
tags: ['Global tag'],
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
tags: [{ name: 'Global tag', description: 'Global tag description' }],
});

const [bundledSpec] = Object.values(
await bundleSpecs(
{
1: spec1,
2: spec2,
},
{
prototypeDocument: {
tags: [
{
name: 'Some Tag',
description: 'Some tag description',
},
{
name: 'Another Tag',
description: 'Another tag description',
},
],
},
}
)
);

expect(bundledSpec.paths['/api/some_api']?.get?.tags).toEqual([
'Some Tag',
'Another Tag',
'Local tag',
]);
expect(bundledSpec.paths['/api/another_api']?.get?.tags).toEqual([
'Some Tag',
'Another Tag',
'Global tag',
]);
expect(bundledSpec.tags).toEqual([
{
name: 'Some Tag',
description: 'Some tag description',
},
{
name: 'Another Tag',
description: 'Another tag description',
},
{ name: 'Global tag', description: 'Global tag description' },
]);
});
});
Loading