[Security Solution] Allow to specify custom servers and security for the result OpenAPI bundle (#189348)

**Resolves:** https://github.com/elastic/kibana/issues/189269
**Resolves:** https://github.com/elastic/kibana/issues/189270

## Summary

This PR adds an ability to specify OpenAPI `servers` and security requirements (`security`) to be used in the result bundle. `servers` and/or `security` in the source OpenAPI specs are be dropped when custom  `servers` and/or `security` provided.

## Details

Kibana is usually deployed at a single access point and manages authentication in a central way. That way it's much more convenient to have control on what `servers` and `security` are present in the result bundles. It will help to avoid conflicts, duplicates and update them in centralized way.

This PR extends OpenAPI bundler configuration options with `prototypeDocument`. "Prototype" in the name means it's a prototype for the result. The bundler uses certain properties from that prototype OpenAPI document to add them to the result OpenAPI bundle. The following properties are used

- `info` representing OpenAPI Info object (former `options.specInfo`)
- `servers` OpenAPI Server Object Array
- `security` + `components.securitySchemes` OpenAPI Security Requirement Object Array + OpenAPI Security Schemes Object (validation checks that both fields are set otherwise an error is thrown)

For convenience `prototypeDocument` could be specified as a string path to a file containing prototype OpenAPI document.

## How to test?

`prototypeDocument` can be specified for `bundle` and `merge` utilities like the following

**bundle**
```js
const { bundle } = require('@kbn/openapi-bundler');

(async () => {
  await bundle({
    sourceGlob: 'source/glob/*.yaml',
    outputFilePath: 'output/bundle.yaml,
    options: {
      prototypeDocument: {
        info: {
          title: 'Some title',
          description: 'Some description',
        },
        servers: [{
          url: 'https://{kibana_url}',
          variables: {
            kibana_url: {
              default: 'localhost:5601',
            }
          }
        }],
        security: [{ ApiKeyAuth: [] }],
        components: {
          securitySchemes: {
            ApiKeyAuth: {
              type: 'apiKey',
              in: 'header',
              name: 'Authorization',
            }
          }
        }
      },
    },
  });
```

**bundle** with external prototype document
```js
const { bundle } = require('@kbn/openapi-bundler');

(async () => {
  await bundle({
    sourceGlob: 'source/glob/*.yaml',
    outputFilePath: 'output/bundle.yaml,
    options: {
      prototypeDocument: 'path/to/prototype_document.yaml',,
    },
  });
```

**merge**
```js
const { merge } = require('@kbn/openapi-bundler');

(async () => {
  await merge({
    sourceGlobs: [
      'absolute/path/to/file.yaml`,
      'some/glob/*.schema.yaml',
    ],
    outputFilePath: 'output/file/path/bundle.yaml',
    options: {
      prototypeDocument: {
        info: {
          title: 'Some title',
          description: 'Some description',
        },
        servers: [{
          url: 'https://{kibana_url}',
          variables: {
            kibana_url: {
              default: 'localhost:5601',
            }
          }
        }],
        security: [{ ApiKeyAuth: [] }],
        components: {
          securitySchemes: {
            ApiKeyAuth: {
              type: 'apiKey',
              in: 'header',
              name: 'Authorization',
            }
          }
        }
      },
    },
  });
})();
```

**merge** with external prototype document
```js
const { merge } = require('@kbn/openapi-bundler');

(async () => {
  await merge({
    sourceGlobs: [
      'absolute/path/to/file.yaml`,
      'some/glob/*.schema.yaml',
    ],
    outputFilePath: 'output/file/path/bundle.yaml',
    options: {
      prototypeDocument: 'path/to/prototype_document.yaml',
    },
  });
})();
```

The result bundles will contain specified `servers` and `security` while source `servers` and `security` will be dropped.
This commit is contained in:
Maxim Palenov 2024-07-31 10:29:11 +02:00 committed by GitHub
parent 9dcd8aad88
commit 51c8949af9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1964 additions and 122 deletions

View file

@ -10,12 +10,32 @@ import { OpenAPIV3 } from 'openapi-types';
export function createBlankOpenApiDocument(
oasVersion: string,
info: OpenAPIV3.InfoObject
overrides?: Partial<OpenAPIV3.Document>
): OpenAPIV3.Document {
return {
openapi: oasVersion,
info,
servers: [
info: overrides?.info ?? {
title: 'Merged OpenAPI specs',
version: 'not specified',
},
paths: overrides?.paths ?? {},
components: {
schemas: overrides?.components?.schemas,
responses: overrides?.components?.responses,
parameters: overrides?.components?.parameters,
examples: overrides?.components?.examples,
requestBodies: overrides?.components?.requestBodies,
headers: overrides?.components?.headers,
securitySchemes: overrides?.components?.securitySchemes ?? {
BasicAuth: {
type: 'http',
scheme: 'basic',
},
},
links: overrides?.components?.links,
callbacks: overrides?.components?.callbacks,
},
servers: overrides?.servers ?? [
{
url: 'http://{kibana_host}:{port}',
variables: {
@ -28,19 +48,12 @@ export function createBlankOpenApiDocument(
},
},
],
security: [
security: overrides?.security ?? [
{
BasicAuth: [],
},
],
paths: {},
components: {
securitySchemes: {
BasicAuth: {
type: 'http',
scheme: 'basic',
},
},
},
tags: overrides?.tags,
externalDocs: overrides?.externalDocs,
};
}

View file

@ -17,8 +17,9 @@ import { mergeTags } from './merge_tags';
import { getOasVersion } from '../../utils/get_oas_version';
import { getOasDocumentVersion } from '../../utils/get_oas_document_version';
import { enrichWithVersionMimeParam } from './enrich_with_version_mime_param';
import { MergeOptions } from './merge_options';
export interface MergeDocumentsOptions {
interface MergeDocumentsOptions extends MergeOptions {
splitDocumentsByVersion: boolean;
}
@ -52,10 +53,20 @@ export async function mergeDocuments(
...documentsGroup,
];
mergedDocument.servers = mergeServers(documentsToMerge);
mergedDocument.paths = mergePaths(documentsToMerge);
mergedDocument.components = mergeSharedComponents(documentsToMerge);
mergedDocument.security = mergeSecurityRequirements(documentsToMerge);
mergedDocument.paths = mergePaths(documentsToMerge, options);
mergedDocument.components = {
...mergedDocument.components,
...mergeSharedComponents(documentsToMerge, options),
};
if (!options.skipServers) {
mergedDocument.servers = mergeServers(documentsToMerge);
}
if (!options.skipSecurity) {
mergedDocument.security = mergeSecurityRequirements(documentsToMerge);
}
mergedDocument.tags = mergeTags(documentsToMerge);
mergedByVersion.set(mergedDocument.info.version, mergedDocument);

View file

@ -11,10 +11,12 @@ import deepEqual from 'fast-deep-equal';
import { OpenAPIV3 } from 'openapi-types';
import { KNOWN_HTTP_METHODS } from './http_methods';
import { isRefNode } from '../process_document';
import { MergeOptions } from './merge_options';
export function mergeOperations(
sourcePathItem: OpenAPIV3.PathItemObject,
mergedPathItem: OpenAPIV3.PathItemObject
mergedPathItem: OpenAPIV3.PathItemObject,
options: MergeOptions
) {
for (const httpMethod of KNOWN_HTTP_METHODS) {
const sourceOperation = sourcePathItem[httpMethod];
@ -24,12 +26,18 @@ export function mergeOperations(
continue;
}
if (!mergedOperation || deepEqual(sourceOperation, mergedOperation)) {
mergedPathItem[httpMethod] = sourceOperation;
const normalizedSourceOperation = {
...sourceOperation,
...(options.skipServers ? { servers: undefined } : { servers: sourceOperation.servers }),
...(options.skipSecurity ? { security: undefined } : { security: sourceOperation.security }),
};
if (!mergedOperation || deepEqual(normalizedSourceOperation, mergedOperation)) {
mergedPathItem[httpMethod] = normalizedSourceOperation;
continue;
}
mergeOperation(sourceOperation, mergedOperation);
mergeOperation(normalizedSourceOperation, mergedOperation);
}
}

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
export interface MergeOptions {
skipServers: boolean;
skipSecurity: boolean;
}

View file

@ -12,8 +12,12 @@ import { ResolvedDocument } from '../ref_resolver/resolved_document';
import { isRefNode } from '../process_document';
import { mergeOperations } from './merge_operations';
import { mergeArrays } from './merge_arrays';
import { MergeOptions } from './merge_options';
export function mergePaths(resolvedDocuments: ResolvedDocument[]): OpenAPIV3.PathsObject {
export function mergePaths(
resolvedDocuments: ResolvedDocument[],
options: MergeOptions
): OpenAPIV3.PathsObject {
const mergedPaths: Record<string, OpenAPIV3.PathItemObject> = {};
for (const { absolutePath, document } of resolvedDocuments) {
@ -60,7 +64,7 @@ export function mergePaths(resolvedDocuments: ResolvedDocument[]): OpenAPIV3.Pat
}
try {
mergeOperations(sourcePathItem, mergedPathItem);
mergeOperations(sourcePathItem, mergedPathItem, options);
} catch (e) {
throw new Error(
`❌ Unable to merge ${chalk.bold(absolutePath)} due to an error in ${chalk.bold(
@ -69,7 +73,9 @@ export function mergePaths(resolvedDocuments: ResolvedDocument[]): OpenAPIV3.Pat
);
}
mergePathItemServers(sourcePathItem, mergedPathItem);
if (!options.skipServers) {
mergePathItemServers(sourcePathItem, mergedPathItem);
}
try {
mergeParameters(sourcePathItem, mergedPathItem);

View file

@ -12,6 +12,7 @@ import { OpenAPIV3 } from 'openapi-types';
import { ResolvedDocument } from '../ref_resolver/resolved_document';
import { extractObjectByJsonPointer } from '../../utils/extract_by_json_pointer';
import { logger } from '../../logger';
import { MergeOptions } from './merge_options';
const MERGEABLE_COMPONENT_TYPES = [
'schemas',
@ -26,11 +27,16 @@ const MERGEABLE_COMPONENT_TYPES = [
] as const;
export function mergeSharedComponents(
bundledDocuments: ResolvedDocument[]
bundledDocuments: ResolvedDocument[],
options: MergeOptions
): OpenAPIV3.ComponentsObject {
const mergedComponents: Record<string, unknown> = {};
for (const componentsType of MERGEABLE_COMPONENT_TYPES) {
if (options.skipSecurity && componentsType === 'securitySchemes') {
continue;
}
const mergedTypedComponents = mergeObjects(bundledDocuments, `/components/${componentsType}`);
if (Object.keys(mergedTypedComponents).length === 0) {

View file

@ -7,8 +7,6 @@
*/
import chalk from 'chalk';
import { isUndefined, omitBy } from 'lodash';
import { OpenAPIV3 } from 'openapi-types';
import { basename, dirname } from 'path';
import { bundleDocument, SkipException } from './bundler/bundle_document';
import { mergeDocuments } from './bundler/merge_documents';
@ -19,6 +17,8 @@ import { writeDocuments } from './utils/write_documents';
import { ResolvedDocument } from './bundler/ref_resolver/resolved_document';
import { resolveGlobs } from './utils/resolve_globs';
import { DEFAULT_BUNDLING_PROCESSORS, withIncludeLabelsProcessor } from './bundler/processor_sets';
import { PrototypeDocument } from './prototype_document';
import { validatePrototypeDocument } from './validate_prototype_document';
export interface BundlerConfig {
sourceGlob: string;
@ -27,8 +27,15 @@ export interface BundlerConfig {
}
interface BundleOptions {
/**
* OpenAPI document itself or path to the document
*/
prototypeDocument?: PrototypeDocument | string;
/**
* When specified the produced bundle will contain only
* operations objects with matching labels
*/
includeLabels?: string[];
specInfo?: Omit<Partial<OpenAPIV3.InfoObject>, 'version'>;
}
export const bundle = async ({
@ -36,6 +43,10 @@ export const bundle = async ({
outputFilePath = 'bundled-{version}.schema.yaml',
options,
}: BundlerConfig) => {
const prototypeDocument = options?.prototypeDocument
? await validatePrototypeDocument(options?.prototypeDocument)
: undefined;
logger.debug(chalk.bold(`Bundling API route schemas`));
logger.debug(`👀 Searching for source files in ${chalk.underline(sourceGlob)}`);
@ -56,22 +67,21 @@ export const bundle = async ({
logger.success(`Processed ${bundledDocuments.length} schemas`);
const blankOasFactory = (oasVersion: string, apiVersion: string) =>
const blankOasDocumentFactory = (oasVersion: string, apiVersion: string) =>
createBlankOpenApiDocument(oasVersion, {
version: apiVersion,
title: options?.specInfo?.title ?? 'Bundled OpenAPI specs',
...omitBy(
{
description: options?.specInfo?.description,
termsOfService: options?.specInfo?.termsOfService,
contact: options?.specInfo?.contact,
license: options?.specInfo?.license,
},
isUndefined
),
info: prototypeDocument?.info
? { ...DEFAULT_INFO, ...prototypeDocument.info, version: apiVersion }
: { ...DEFAULT_INFO, version: apiVersion },
servers: prototypeDocument?.servers,
security: prototypeDocument?.security,
components: {
securitySchemes: prototypeDocument?.components?.securitySchemes,
},
});
const resultDocumentsMap = await mergeDocuments(bundledDocuments, blankOasFactory, {
const resultDocumentsMap = await mergeDocuments(bundledDocuments, blankOasDocumentFactory, {
splitDocumentsByVersion: true,
skipServers: Boolean(prototypeDocument?.servers),
skipSecurity: Boolean(prototypeDocument?.security),
});
await writeDocuments(resultDocumentsMap, outputFilePath);
@ -130,3 +140,7 @@ function filterOutSkippedDocuments(
return processedDocuments;
}
const DEFAULT_INFO = {
title: 'Bundled OpenAPI specs',
} as const;

View file

@ -7,7 +7,7 @@
*/
import chalk from 'chalk';
import { OpenAPIV3 } from 'openapi-types';
import { mergeDocuments } from './bundler/merge_documents';
import { logger } from './logger';
import { createBlankOpenApiDocument } from './bundler/merge_documents/create_blank_oas_document';
@ -16,13 +16,20 @@ import { writeDocuments } from './utils/write_documents';
import { resolveGlobs } from './utils/resolve_globs';
import { bundleDocument } from './bundler/bundle_document';
import { withNamespaceComponentsProcessor } from './bundler/processor_sets';
import { PrototypeDocument } from './prototype_document';
import { validatePrototypeDocument } from './validate_prototype_document';
export interface MergerConfig {
sourceGlobs: string[];
outputFilePath: string;
options?: {
mergedSpecInfo?: Partial<OpenAPIV3.InfoObject>;
};
options?: MergerOptions;
}
interface MergerOptions {
/**
* OpenAPI document itself or path to the document
*/
prototypeDocument?: PrototypeDocument | string;
}
export const merge = async ({
@ -34,6 +41,10 @@ export const merge = async ({
throw new Error('As minimum one source glob is expected');
}
const prototypeDocument = options?.prototypeDocument
? await validatePrototypeDocument(options?.prototypeDocument)
: undefined;
logger.info(chalk.bold(`Merging OpenAPI specs`));
logger.info(
`👀 Searching for source files in ${sourceGlobs
@ -52,13 +63,18 @@ export const merge = async ({
const blankOasDocumentFactory = (oasVersion: string) =>
createBlankOpenApiDocument(oasVersion, {
title: 'Merged OpenAPI specs',
version: 'not specified',
...(options?.mergedSpecInfo ?? {}),
info: prototypeDocument?.info ? { ...DEFAULT_INFO, ...prototypeDocument.info } : DEFAULT_INFO,
servers: prototypeDocument?.servers,
security: prototypeDocument?.security,
components: {
securitySchemes: prototypeDocument?.components?.securitySchemes,
},
});
const resultDocumentsMap = await mergeDocuments(bundledDocuments, blankOasDocumentFactory, {
splitDocumentsByVersion: false,
skipServers: Boolean(prototypeDocument?.servers),
skipSecurity: Boolean(prototypeDocument?.security),
});
// Only one document is expected when `splitDocumentsByVersion` is set to `false`
const mergedDocument = Array.from(resultDocumentsMap.values())[0];
@ -80,3 +96,8 @@ async function bundleDocuments(schemaFilePaths: string[]): Promise<ResolvedDocum
)
);
}
const DEFAULT_INFO = {
title: 'Merged OpenAPI specs',
version: 'not specified',
} as const;

View file

@ -0,0 +1,29 @@
/*
* 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 { 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.
*/
export interface PrototypeDocument {
info?: Partial<OpenAPIV3.InfoObject>;
servers?: OpenAPIV3.ServerObject[];
security?: OpenAPIV3.SecurityRequirementObject[];
components?: {
securitySchemes: Record<string, OpenAPIV3.SecuritySchemeObject>;
};
}

View file

@ -7,37 +7,48 @@
*/
import fs from 'fs/promises';
import { load } from 'js-yaml';
import { basename, extname } from 'path';
import { load } from 'js-yaml';
import chalk from 'chalk';
import { logger } from '../logger';
import { isPlainObjectType } from './is_plain_object_type';
export async function readDocument(documentPath: string): Promise<Record<string, unknown>> {
const extension = extname(documentPath);
logger.debug(`Reading ${chalk.bold(basename(documentPath))}`);
const maybeDocument = await readFile(documentPath);
if (!isPlainObjectType(maybeDocument)) {
throw new Error(`File at ${chalk.bold(documentPath)} is not valid OpenAPI document`);
}
return maybeDocument;
}
async function readFile(filePath: string): Promise<unknown> {
const extension = extname(filePath);
switch (extension) {
case '.yaml':
case '.yml':
return await readYamlDocument(documentPath);
return await readYamlFile(filePath);
case '.json':
return await readJsonDocument(documentPath);
return await readJsonFile(filePath);
default:
throw new Error(`${extension} files are not supported`);
}
}
async function readYamlDocument(filePath: string): Promise<Record<string, unknown>> {
async function readYamlFile(filePath: string): Promise<Record<string, unknown>> {
// Typing load's result to Record<string, unknown> is optimistic as we can't be sure
// there is object inside a yaml file. We don't have this validation layer so far
// but using JSON Schemas here should mitigate this problem.
return load(await fs.readFile(filePath, { encoding: 'utf8' }));
}
export async function readJsonDocument(filePath: string): Promise<Record<string, unknown>> {
async function readJsonFile(filePath: string): Promise<Record<string, unknown>> {
// Typing load's result to Record<string, unknown> is optimistic as we can't be sure
// there is object inside a yaml file. We don't have this validation layer so far
// but using JSON Schemas here should mitigate this problem.

View file

@ -0,0 +1,45 @@
/*
* 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 chalk from 'chalk';
import { PrototypeDocument } from './prototype_document';
import { readDocument } from './utils/read_document';
/**
* Validates that passed `prototypeDocument` fulfills the requirements.
*
* In particular security requirements must be specified via `security` and
* `components.securitySchemes` properties.
*
*/
export async function validatePrototypeDocument(
prototypeDocumentOrString: PrototypeDocument | string
): Promise<PrototypeDocument> {
const prototypeDocument: PrototypeDocument | undefined =
typeof prototypeDocumentOrString === 'string'
? await readDocument(prototypeDocumentOrString)
: prototypeDocumentOrString;
if (prototypeDocument.security && !prototypeDocument.components?.securitySchemes) {
throw new Error(
`Prototype document must contain ${chalk.bold(
'components.securitySchemes'
)} when security requirements are specified`
);
}
if (prototypeDocument.components?.securitySchemes && !prototypeDocument.security) {
throw new Error(
`Prototype document must have ${chalk.bold('security')} defined ${chalk.bold(
'components.securitySchemes'
)} are specified`
);
}
return prototypeDocument;
}

View file

@ -0,0 +1,403 @@
/*
* 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 chalk from 'chalk';
import { OpenAPIV3 } from 'openapi-types';
import { createOASDocument } from '../../create_oas_document';
import { bundleSpecs } from '../bundle_specs';
describe('OpenAPI Bundler - with security requirements overrides', () => {
describe('enabled', () => {
it('throws an error when security requirements are specified without components security schemes', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {},
},
},
},
});
await expect(
bundleSpecs(
{
1: spec1,
},
{
prototypeDocument: {
security: [{ ShouldBeUsedSecurityRequirement: [] }],
},
}
)
).rejects.toThrowError(
`Prototype document must contain ${chalk.bold(
'components.securitySchemes'
)} when security requirements are specified`
);
});
it('throws an error when components security schemes are specified without security requirements', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {},
},
},
},
});
await expect(
bundleSpecs(
{
1: spec1,
},
{
prototypeDocument: {
components: {
securitySchemes: {
ShouldBeUsedSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
},
},
},
}
)
).rejects.toThrowError(
`Prototype document must have ${chalk.bold('security')} defined ${chalk.bold(
'components.securitySchemes'
)} are specified`
);
});
it('overrides root level `security`', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
security: [{ SomeSecurityRequirement: [] }],
});
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
security: [{ AnotherSecurityRequirement: [] }, { AdditionalSecurityRequirement: [] }],
});
const [bundledSpec] = Object.values(
await bundleSpecs(
{
1: spec1,
2: spec2,
},
{
prototypeDocument: {
security: [{ ShouldBeUsedSecurityRequirement: [] }],
components: {
securitySchemes: {
ShouldBeUsedSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
},
},
},
}
)
);
expect(bundledSpec.security).toEqual([{ ShouldBeUsedSecurityRequirement: [] }]);
expect(bundledSpec.components?.securitySchemes).toEqual({
ShouldBeUsedSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
});
});
it('drops operation level security requirements', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
security: [{ SomeSecurityRequirement: [] }],
},
},
},
});
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
security: [{ AnotherSecurityRequirement: [] }, { AdditionalSecurityRequirement: [] }],
},
},
},
});
const [bundledSpec] = Object.values(
await bundleSpecs(
{
1: spec1,
2: spec2,
},
{
prototypeDocument: {
security: [{ ShouldBeUsedSecurityRequirement: [] }],
components: {
securitySchemes: {
ShouldBeUsedSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
},
},
},
}
)
);
expect(bundledSpec.paths['/api/some_api']?.get?.security).toBeUndefined();
expect(bundledSpec.paths['/api/another_api']?.get?.security).toBeUndefined();
});
});
describe('disabled', () => {
it('bundles root level security requirements', async () => {
const spec1Security = [{ SomeSecurityRequirement: [] }];
const spec1SecuritySchemes = {
SomeSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
} as const;
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
security: spec1Security,
components: {
securitySchemes: spec1SecuritySchemes,
},
});
const spec2Security: OpenAPIV3.SecurityRequirementObject[] = [
{ AnotherSecurityRequirement: [] },
{ AdditionalSecurityRequirement: [] },
];
const spec2SecuritySchemes = {
AnotherSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
AdditionalSecurityRequirement: {
type: 'apiKey',
name: 'apiKey',
in: 'header',
},
} as const;
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
security: spec2Security,
components: {
securitySchemes: spec2SecuritySchemes,
},
});
const [bundledSpec] = Object.values(
await bundleSpecs({
1: spec1,
2: spec2,
})
);
expect(bundledSpec.security).toEqual(
expect.arrayContaining([...spec1Security, ...spec2Security])
);
expect(bundledSpec.components?.securitySchemes).toMatchObject({
...spec1SecuritySchemes,
...spec2SecuritySchemes,
});
});
it('bundles operation level security requirements', async () => {
const spec1Security = [{ SomeSecurityRequirement: [] }];
const spec1SecuritySchemes = {
SomeSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
} as const;
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
security: spec1Security,
},
},
},
components: {
securitySchemes: spec1SecuritySchemes,
},
});
const spec2Security: OpenAPIV3.SecurityRequirementObject[] = [
{ AnotherSecurityRequirement: [] },
{ AdditionalSecurityRequirement: [] },
];
const spec2SecuritySchemes = {
AnotherSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
AdditionalSecurityRequirement: {
type: 'apiKey',
name: 'apiKey',
in: 'header',
},
} as const;
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
security: spec2Security,
},
},
},
components: {
securitySchemes: spec2SecuritySchemes,
},
});
const [bundledSpec] = Object.values(
await bundleSpecs({
1: spec1,
2: spec2,
})
);
expect(bundledSpec.paths['/api/some_api']?.get?.security).toEqual(spec1Security);
expect(bundledSpec.paths['/api/another_api']?.get?.security).toEqual(spec2Security);
expect(bundledSpec.components?.securitySchemes).toMatchObject({
...spec1SecuritySchemes,
...spec2SecuritySchemes,
});
});
});
});

View file

@ -0,0 +1,409 @@
/*
* 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 - with `servers` overrides', () => {
describe('enabled', () => {
it('overrides root level `servers`', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
servers: [{ url: 'https://some-url' }],
});
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
servers: [
{ url: 'https://another-url', description: 'some description' },
{ url: 'https://something-else-url', description: 'some description' },
],
});
const [bundledSpec] = Object.values(
await bundleSpecs(
{
1: spec1,
2: spec2,
},
{
prototypeDocument: {
servers: [
{ url: 'https://should-be-used-url', description: 'Should be used description' },
],
},
}
)
);
expect(bundledSpec.servers).toEqual([
{ url: 'https://should-be-used-url', description: 'Should be used description' },
]);
});
it('drops path level `servers`', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
servers: [{ url: 'https://some-url' }],
},
},
});
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
servers: [
{ url: 'https://another-url', description: 'some description' },
{ url: 'https://something-else-url', description: 'some description' },
],
},
},
});
const [bundledSpec] = Object.values(
await bundleSpecs(
{
1: spec1,
2: spec2,
},
{
prototypeDocument: {
servers: [
{ url: 'https://should-be-used-url', description: 'Should be used description' },
],
},
}
)
);
expect(bundledSpec.paths['/api/some_api']?.servers).toBeUndefined();
expect(bundledSpec.paths['/api/another_api']?.servers).toBeUndefined();
});
it('drops operation level `servers`', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
servers: [{ url: 'https://some-url' }],
},
},
},
});
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
servers: [
{ url: 'https://another-url', description: 'some description' },
{ url: 'https://something-else-url', description: 'some description' },
],
},
},
},
});
const [bundledSpec] = Object.values(
await bundleSpecs(
{
1: spec1,
2: spec2,
},
{
prototypeDocument: {
servers: [
{ url: 'https://should-be-used-url', description: 'Should be used description' },
],
},
}
)
);
expect(bundledSpec.paths['/api/some_api']?.get?.servers).toBeUndefined();
expect(bundledSpec.paths['/api/another_api']?.get?.servers).toBeUndefined();
});
});
describe('disabled', () => {
it('bundles root level `servers`', async () => {
const spec1Servers = [{ url: 'https://some-url' }];
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
servers: spec1Servers,
});
const spec2Servers = [
{ url: 'https://another-url', description: 'some description' },
{ url: 'https://something-else-url', description: 'some description' },
];
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
servers: spec2Servers,
});
const [bundledSpec] = Object.values(
await bundleSpecs({
1: spec1,
2: spec2,
})
);
const DEFAULT_ENTRY = {
url: 'http://{kibana_host}:{port}',
variables: {
kibana_host: {
default: 'localhost',
},
port: {
default: '5601',
},
},
};
expect(bundledSpec.servers).toEqual([DEFAULT_ENTRY, ...spec1Servers, ...spec2Servers]);
});
it('bundles path level `servers`', async () => {
const spec1Servers = [{ url: 'https://some-url' }];
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
servers: spec1Servers,
},
},
});
const spec2Servers = [
{ url: 'https://another-url', description: 'some description' },
{ url: 'https://something-else-url', description: 'some description' },
];
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
servers: spec2Servers,
},
},
});
const [bundledSpec] = Object.values(
await bundleSpecs({
1: spec1,
2: spec2,
})
);
expect(bundledSpec.paths['/api/some_api']?.servers).toEqual(spec1Servers);
expect(bundledSpec.paths['/api/another_api']?.servers).toEqual(spec2Servers);
});
it('bundles operation level `servers`', async () => {
const spec1Servers = [{ url: 'https://some-url' }];
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
servers: spec1Servers,
},
},
},
});
const spec2Servers = [
{ url: 'https://another-url', description: 'some description' },
{ url: 'https://something-else-url', description: 'some description' },
];
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
servers: spec2Servers,
},
},
},
});
const [bundledSpec] = Object.values(
await bundleSpecs({
1: spec1,
2: spec2,
})
);
expect(bundledSpec.paths['/api/some_api']?.get?.servers).toEqual(spec1Servers);
expect(bundledSpec.paths['/api/another_api']?.get?.servers).toEqual(spec2Servers);
});
});
});

View file

@ -0,0 +1,415 @@
/*
* 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 chalk from 'chalk';
import { createOASDocument } from '../../create_oas_document';
import { mergeSpecs } from '../merge_specs';
// Disable naming convention check due to tests on spec title prefixes
// like Spec1_Something which violates that rule
/* eslint-disable @typescript-eslint/naming-convention */
describe('OpenAPI Merger - with security requirements overrides', () => {
describe('enabled', () => {
it('throws an error when security requirements are specified without components security schemes', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {},
},
},
},
});
await expect(
mergeSpecs(
{
1: spec1,
},
{
prototypeDocument: {
security: [{ ShouldBeUsedSecurityRequirement: [] }],
},
}
)
).rejects.toThrowError(
`Prototype document must contain ${chalk.bold(
'components.securitySchemes'
)} when security requirements are specified`
);
});
it('throws an error when components security schemes are specified without security requirements', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {},
},
},
},
});
await expect(
mergeSpecs(
{
1: spec1,
},
{
prototypeDocument: {
components: {
securitySchemes: {
ShouldBeUsedSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
},
},
},
}
)
).rejects.toThrowError(
`Prototype document must have ${chalk.bold('security')} defined ${chalk.bold(
'components.securitySchemes'
)} are specified`
);
});
it('overrides root level `security`', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
security: [{ SomeSecurityRequirement: [] }],
});
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
security: [{ AnotherSecurityRequirement: [] }, { AdditionalSecurityRequirement: [] }],
});
const [bundledSpec] = Object.values(
await mergeSpecs(
{
1: spec1,
2: spec2,
},
{
prototypeDocument: {
security: [{ ShouldBeUsedSecurityRequirement: [] }],
components: {
securitySchemes: {
ShouldBeUsedSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
},
},
},
}
)
);
expect(bundledSpec.security).toEqual([{ ShouldBeUsedSecurityRequirement: [] }]);
expect(bundledSpec.components?.securitySchemes).toEqual({
ShouldBeUsedSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
});
});
it('drops operation level security requirements', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
security: [{ SomeSecurityRequirement: [] }],
},
},
},
});
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
security: [{ AnotherSecurityRequirement: [] }, { AdditionalSecurityRequirement: [] }],
},
},
},
});
const [bundledSpec] = Object.values(
await mergeSpecs(
{
1: spec1,
2: spec2,
},
{
prototypeDocument: {
security: [{ ShouldBeUsedSecurityRequirement: [] }],
components: {
securitySchemes: {
ShouldBeUsedSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
},
},
},
}
)
);
expect(bundledSpec.paths['/api/some_api']?.get?.security).toBeUndefined();
expect(bundledSpec.paths['/api/another_api']?.get?.security).toBeUndefined();
});
});
describe('disabled', () => {
it('bundles root level security requirements', async () => {
const spec1 = createOASDocument({
info: {
title: 'Spec1',
},
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
security: [{ SomeSecurityRequirement: [] }],
components: {
securitySchemes: {
SomeSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
},
},
});
const spec2 = createOASDocument({
info: {
title: 'Spec2',
},
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
security: [{ AnotherSecurityRequirement: [] }, { AdditionalSecurityRequirement: [] }],
components: {
securitySchemes: {
AnotherSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
AdditionalSecurityRequirement: {
type: 'apiKey',
name: 'apiKey',
in: 'header',
},
},
},
});
const [bundledSpec] = Object.values(
await mergeSpecs({
1: spec1,
2: spec2,
})
);
expect(bundledSpec.security).toEqual(
expect.arrayContaining([
{ Spec1_SomeSecurityRequirement: [] },
{ Spec2_AnotherSecurityRequirement: [] },
{ Spec2_AdditionalSecurityRequirement: [] },
])
);
expect(bundledSpec.components?.securitySchemes).toMatchObject({
Spec1_SomeSecurityRequirement: expect.anything(),
Spec2_AnotherSecurityRequirement: expect.anything(),
Spec2_AdditionalSecurityRequirement: expect.anything(),
});
});
it('bundles operation level security requirements', async () => {
const spec1 = createOASDocument({
info: {
title: 'Spec1',
},
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
security: [{ SomeSecurityRequirement: [] }],
},
},
},
components: {
securitySchemes: {
SomeSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
},
},
});
const spec2 = createOASDocument({
info: {
title: 'Spec2',
},
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
security: [{ AnotherSecurityRequirement: [] }, { AdditionalSecurityRequirement: [] }],
},
},
},
components: {
securitySchemes: {
AnotherSecurityRequirement: {
type: 'http',
scheme: 'basic',
},
AdditionalSecurityRequirement: {
type: 'apiKey',
name: 'apiKey',
in: 'header',
},
},
},
});
const [bundledSpec] = Object.values(
await mergeSpecs({
1: spec1,
2: spec2,
})
);
expect(bundledSpec.paths['/api/some_api']?.get?.security).toEqual([
{ Spec1_SomeSecurityRequirement: [] },
]);
expect(bundledSpec.paths['/api/another_api']?.get?.security).toEqual([
{ Spec2_AnotherSecurityRequirement: [] },
{ Spec2_AdditionalSecurityRequirement: [] },
]);
expect(bundledSpec.components?.securitySchemes).toMatchObject({
Spec1_SomeSecurityRequirement: expect.anything(),
Spec2_AnotherSecurityRequirement: expect.anything(),
Spec2_AdditionalSecurityRequirement: expect.anything(),
});
});
});
});

View file

@ -0,0 +1,409 @@
/*
* 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 { mergeSpecs } from '../merge_specs';
describe('OpenAPI Merger - with `servers` overrides', () => {
describe('enabled', () => {
it('overrides root level `servers`', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
servers: [{ url: 'https://some-url' }],
});
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
servers: [
{ url: 'https://another-url', description: 'some description' },
{ url: 'https://something-else-url', description: 'some description' },
],
});
const [bundledSpec] = Object.values(
await mergeSpecs(
{
1: spec1,
2: spec2,
},
{
prototypeDocument: {
servers: [
{ url: 'https://should-be-used-url', description: 'Should be used description' },
],
},
}
)
);
expect(bundledSpec.servers).toEqual([
{ url: 'https://should-be-used-url', description: 'Should be used description' },
]);
});
it('drops path level `servers`', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
servers: [{ url: 'https://some-url' }],
},
},
});
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
servers: [
{ url: 'https://another-url', description: 'some description' },
{ url: 'https://something-else-url', description: 'some description' },
],
},
},
});
const [bundledSpec] = Object.values(
await mergeSpecs(
{
1: spec1,
2: spec2,
},
{
prototypeDocument: {
servers: [
{ url: 'https://should-be-used-url', description: 'Should be used description' },
],
},
}
)
);
expect(bundledSpec.paths['/api/some_api']?.servers).toBeUndefined();
expect(bundledSpec.paths['/api/another_api']?.servers).toBeUndefined();
});
it('drops operation level `servers`', async () => {
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
servers: [{ url: 'https://some-url' }],
},
},
},
});
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
servers: [
{ url: 'https://another-url', description: 'some description' },
{ url: 'https://something-else-url', description: 'some description' },
],
},
},
},
});
const [bundledSpec] = Object.values(
await mergeSpecs(
{
1: spec1,
2: spec2,
},
{
prototypeDocument: {
servers: [
{ url: 'https://should-be-used-url', description: 'Should be used description' },
],
},
}
)
);
expect(bundledSpec.paths['/api/some_api']?.get?.servers).toBeUndefined();
expect(bundledSpec.paths['/api/another_api']?.get?.servers).toBeUndefined();
});
});
describe('disabled', () => {
it('bundles root level `servers`', async () => {
const spec1Servers = [{ url: 'https://some-url' }];
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
servers: spec1Servers,
});
const spec2Servers = [
{ url: 'https://another-url', description: 'some description' },
{ url: 'https://something-else-url', description: 'some description' },
];
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
},
},
servers: spec2Servers,
});
const [bundledSpec] = Object.values(
await mergeSpecs({
1: spec1,
2: spec2,
})
);
const DEFAULT_ENTRY = {
url: 'http://{kibana_host}:{port}',
variables: {
kibana_host: {
default: 'localhost',
},
port: {
default: '5601',
},
},
};
expect(bundledSpec.servers).toEqual([DEFAULT_ENTRY, ...spec1Servers, ...spec2Servers]);
});
it('bundles path level `servers`', async () => {
const spec1Servers = [{ url: 'https://some-url' }];
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
servers: spec1Servers,
},
},
});
const spec2Servers = [
{ url: 'https://another-url', description: 'some description' },
{ url: 'https://something-else-url', description: 'some description' },
];
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
},
servers: spec2Servers,
},
},
});
const [bundledSpec] = Object.values(
await mergeSpecs({
1: spec1,
2: spec2,
})
);
expect(bundledSpec.paths['/api/some_api']?.servers).toEqual(spec1Servers);
expect(bundledSpec.paths['/api/another_api']?.servers).toEqual(spec2Servers);
});
it('bundles operation level `servers`', async () => {
const spec1Servers = [{ url: 'https://some-url' }];
const spec1 = createOASDocument({
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
servers: spec1Servers,
},
},
},
});
const spec2Servers = [
{ url: 'https://another-url', description: 'some description' },
{ url: 'https://something-else-url', description: 'some description' },
];
const spec2 = createOASDocument({
paths: {
'/api/another_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
},
},
servers: spec2Servers,
},
},
},
});
const [bundledSpec] = Object.values(
await mergeSpecs({
1: spec1,
2: spec2,
})
);
expect(bundledSpec.paths['/api/some_api']?.get?.servers).toEqual(spec1Servers);
expect(bundledSpec.paths['/api/another_api']?.get?.servers).toEqual(spec2Servers);
});
});
});

View file

@ -21,10 +21,12 @@ const ROOT = resolve(__dirname, '..');
),
options: {
includeLabels: ['serverless'],
specInfo: {
title: 'Security Solution Exceptions API (Elastic Cloud Serverless)',
description:
"Exceptions API allows you to manage detection rule exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.",
prototypeDocument: {
info: {
title: 'Security Solution Exceptions API (Elastic Cloud Serverless)',
description:
"Exceptions API allows you to manage detection rule exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.",
},
},
},
});
@ -37,10 +39,12 @@ const ROOT = resolve(__dirname, '..');
),
options: {
includeLabels: ['ess'],
specInfo: {
title: 'Security Solution Exceptions API (Elastic Cloud and self-hosted)',
description:
"Exceptions API allows you to manage detection rule exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.",
prototypeDocument: {
info: {
title: 'Security Solution Exceptions API (Elastic Cloud and self-hosted)',
description:
"Exceptions API allows you to manage detection rule exceptions to prevent a rule from generating an alert from incoming events even when the rule's other criteria are met.",
},
},
},
});

View file

@ -21,9 +21,11 @@ const ROOT = resolve(__dirname, '..');
),
options: {
includeLabels: ['serverless'],
specInfo: {
title: 'Security Solution Lists API (Elastic Cloud Serverless)',
description: 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.',
prototypeDocument: {
info: {
title: 'Security Solution Lists API (Elastic Cloud Serverless)',
description: 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.',
},
},
},
});
@ -36,9 +38,11 @@ const ROOT = resolve(__dirname, '..');
),
options: {
includeLabels: ['ess'],
specInfo: {
title: 'Security Solution Lists API (Elastic Cloud and self-hosted)',
description: 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.',
prototypeDocument: {
info: {
title: 'Security Solution Lists API (Elastic Cloud and self-hosted)',
description: 'Lists API allows you to manage lists of keywords, IPs or IP ranges items.',
},
},
},
});

View file

@ -21,9 +21,11 @@ const ELASTIC_ASSISTANT_ROOT = resolve(__dirname, '../..');
),
options: {
includeLabels: ['serverless'],
specInfo: {
title: 'Security AI Assistant API (Elastic Cloud Serverless)',
description: 'Manage and interact with Security Assistant resources.',
prototypeDocument: {
info: {
title: 'Security AI Assistant API (Elastic Cloud Serverless)',
description: 'Manage and interact with Security Assistant resources.',
},
},
},
});
@ -36,9 +38,11 @@ const ELASTIC_ASSISTANT_ROOT = resolve(__dirname, '../..');
),
options: {
includeLabels: ['ess'],
specInfo: {
title: 'Security AI Assistant API (Elastic Cloud & self-hosted)',
description: 'Manage and interact with Security Assistant resources.',
prototypeDocument: {
info: {
title: 'Security AI Assistant API (Elastic Cloud & self-hosted)',
description: 'Manage and interact with Security Assistant resources.',
},
},
},
});

View file

@ -20,9 +20,11 @@ const ELASTIC_ASSISTANT_ROOT = resolve(__dirname, '../..');
outputFilePath: 'docs/openapi/serverless/osquery_api_{version}.bundled.schema.yaml',
options: {
includeLabels: ['serverless'],
specInfo: {
title: 'Security Solution Osquery API (Elastic Cloud Serverless)',
description: 'Run live queries, manage packs and saved queries.',
prototypeDocument: {
info: {
title: 'Security Solution Osquery API (Elastic Cloud Serverless)',
description: 'Run live queries, manage packs and saved queries.',
},
},
},
});
@ -33,9 +35,11 @@ const ELASTIC_ASSISTANT_ROOT = resolve(__dirname, '../..');
outputFilePath: 'docs/openapi/ess/osquery_api_{version}.bundled.schema.yaml',
options: {
includeLabels: ['ess'],
specInfo: {
title: 'Security Solution Osquery API (Elastic Cloud and self-hosted)',
description: 'Run live queries, manage packs and saved queries.',
prototypeDocument: {
info: {
title: 'Security Solution Osquery API (Elastic Cloud and self-hosted)',
description: 'Run live queries, manage packs and saved queries.',
},
},
},
});

View file

@ -20,10 +20,12 @@ const ROOT = resolve(__dirname, '../..');
),
options: {
includeLabels: ['serverless'],
specInfo: {
title: 'Security Solution Detections API (Elastic Cloud Serverless)',
description:
'You can create rules that automatically turn events and external alerts sent to Elastic Security into detection alerts. These alerts are displayed on the Detections page.',
prototypeDocument: {
info: {
title: 'Security Solution Detections API (Elastic Cloud Serverless)',
description:
'You can create rules that automatically turn events and external alerts sent to Elastic Security into detection alerts. These alerts are displayed on the Detections page.',
},
},
},
});
@ -36,10 +38,12 @@ const ROOT = resolve(__dirname, '../..');
),
options: {
includeLabels: ['ess'],
specInfo: {
title: 'Security Solution Detections API (Elastic Cloud and self-hosted)',
description:
'You can create rules that automatically turn events and external alerts sent to Elastic Security into detection alerts. These alerts are displayed on the Detections page.',
prototypeDocument: {
info: {
title: 'Security Solution Detections API (Elastic Cloud and self-hosted)',
description:
'You can create rules that automatically turn events and external alerts sent to Elastic Security into detection alerts. These alerts are displayed on the Detections page.',
},
},
},
});

View file

@ -20,9 +20,11 @@ const ROOT = resolve(__dirname, '../..');
),
options: {
includeLabels: ['serverless'],
specInfo: {
title: 'Security Solution Endpoint Management API (Elastic Cloud Serverless)',
description: 'Interact with and manage endpoints running the Elastic Defend integration.',
prototypeDocument: {
info: {
title: 'Security Solution Endpoint Management API (Elastic Cloud Serverless)',
description: 'Interact with and manage endpoints running the Elastic Defend integration.',
},
},
},
});
@ -35,9 +37,11 @@ const ROOT = resolve(__dirname, '../..');
),
options: {
includeLabels: ['ess'],
specInfo: {
title: 'Security Solution Endpoint Management API (Elastic Cloud and self-hosted)',
description: 'Interact with and manage endpoints running the Elastic Defend integration.',
prototypeDocument: {
info: {
title: 'Security Solution Endpoint Management API (Elastic Cloud and self-hosted)',
description: 'Interact with and manage endpoints running the Elastic Defend integration.',
},
},
},
});

View file

@ -11,32 +11,38 @@ const { join, resolve } = require('path');
const ROOT = resolve(__dirname, '../..');
bundle({
sourceGlob: join(ROOT, 'common/api/entity_analytics/**/*.schema.yaml'),
outputFilePath: join(
ROOT,
'docs/openapi/serverless/security_solution_entity_analytics_api_{version}.bundled.schema.yaml'
),
options: {
includeLabels: ['serverless'],
specInfo: {
title: 'Security Solution Entity Analytics API (Elastic Cloud Serverless)',
description: '',
(async () => {
await bundle({
sourceGlob: join(ROOT, 'common/api/entity_analytics/**/*.schema.yaml'),
outputFilePath: join(
ROOT,
'docs/openapi/serverless/security_solution_entity_analytics_api_{version}.bundled.schema.yaml'
),
options: {
includeLabels: ['serverless'],
prototypeDocument: {
info: {
title: 'Security Solution Entity Analytics API (Elastic Cloud Serverless)',
description: '',
},
},
},
},
});
});
bundle({
sourceGlob: join(ROOT, 'common/api/entity_analytics/**/*.schema.yaml'),
outputFilePath: join(
ROOT,
'docs/openapi/ess/security_solution_entity_analytics_api_{version}.bundled.schema.yaml'
),
options: {
includeLabels: ['ess'],
specInfo: {
title: 'Security Solution Entity Analytics API (Elastic Cloud and self-hosted)',
description: '',
await bundle({
sourceGlob: join(ROOT, 'common/api/entity_analytics/**/*.schema.yaml'),
outputFilePath: join(
ROOT,
'docs/openapi/ess/security_solution_entity_analytics_api_{version}.bundled.schema.yaml'
),
options: {
includeLabels: ['ess'],
prototypeDocument: {
info: {
title: 'Security Solution Entity Analytics API (Elastic Cloud and self-hosted)',
description: '',
},
},
},
},
});
});
})();