[Security Solution] Prefix local references in mapping with a namespace (#189472)

**Resolves:** https://github.com/elastic/kibana/issues/188817

## Summary

This PR handles OpenAPI discriminator `mapping` (missing in https://github.com/elastic/kibana/pull/188812) field by prefixing local references with a namespace (see https://github.com/elastic/kibana/pull/188812 for more namespace details). It throws an error If mapping uses external references.

## How to test?

Let's consider the following OpenAPI spec

**spec1.schema.yaml**
```yaml
openapi: 3.0.3
info:
  title: Spec1
  version: '2023-10-31'
paths:
  /api/some_api:
    get:
      responses:
        200:
          content:
            'application/json':
              schema:
                oneOf:
                  - $ref: '#/components/schemas/Cat'
                  - $ref: '#/components/schemas/Dog'
                  - $ref: '#/components/schemas/Lizard'
                discriminator:
                  propertyName: petType
                  mapping:
                    dog: '#/components/schemas/Dog'
components:
  schemas:
    Pet:
      type: object
      required: [petType]
      properties:
        petType:
          type: string
    Cat:
      allOf:
        - $ref: '#/components/schemas/Pet'
        - type: object
          properties:
            name:
              type: string
    Dog:
      allOf:
        - $ref: '#/components/schemas/Pet'
        - type: object
          properties:
            bark:
              type: string
    Lizard:
      allOf:
        - $ref: '#/components/schemas/Pet'
        - type: object
          properties:
            lovesRocks:
              type: boolean
```

and a merging script

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

(async () => {
  await merge({
    sourceGlobs: [
      `path/to/spec1.schema.yaml`,
    ],
    outputFilePath: 'output.yaml,
  });
})();
```

After running it it will produce the following bundler with references in `mapping` property prefixed with the spec title.

```yaml
openapi: 3.0.3
info:
  title: Some title
  version: 1
paths:
  /api/some_api:
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                discriminator:
                  mapping:
                    dog: '#/components/schemas/Spec1_Dog'
                  propertyName: petType
                oneOf:
                  - $ref: '#/components/schemas/Spec1_Cat'
                  - $ref: '#/components/schemas/Spec1_Dog'
                  - $ref: '#/components/schemas/Spec1_Lizard'
components:
  schemas:
    Spec1_Cat:
      allOf:
        - $ref: '#/components/schemas/Spec1_Pet'
        - type: object
          properties:
            name:
              type: string
    Spec1_Dog:
      allOf:
        - $ref: '#/components/schemas/Spec1_Pet'
        - type: object
          properties:
            bark:
              type: string
    Spec1_Lizard:
      allOf:
        - $ref: '#/components/schemas/Spec1_Pet'
        - type: object
          properties:
            lovesRocks:
              type: boolean
    Spec1_Pet:
      type: object
      properties:
        petType:
          type: string
      required:
        - petType
```
This commit is contained in:
Maxim Palenov 2024-07-30 21:39:28 +02:00 committed by GitHub
parent 6c581d541b
commit 934d0b1cbc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 145 additions and 32 deletions

View file

@ -10,6 +10,7 @@ import { extractByJsonPointer } from '../../../utils/extract_by_json_pointer';
import { isPlainObjectType } from '../../../utils/is_plain_object_type';
import { parseRef } from '../../../utils/parse_ref';
import { DocumentNodeProcessor } from './types/document_node_processor';
import { isLocalRef } from './utils/is_local_ref';
/**
* Creates a node processor to prefix possibly conflicting components and security requirements
@ -58,6 +59,23 @@ export function createNamespaceComponentsProcessor(pointer: string): DocumentNod
// `components.securitySchemes`. It means items in `security` implicitly reference
// `components.securitySchemes` items which should be handled.
onNodeLeave(node, context) {
// Handle mappings
if (context.parentKey === 'mapping' && isPlainObjectType(node)) {
for (const key of Object.keys(node)) {
const maybeRef = node[key];
if (typeof maybeRef !== 'string' || !isLocalRef(maybeRef)) {
throw new Error(
`Expected mappings to have local references but got "${maybeRef}" in ${JSON.stringify(
node
)}`
);
}
node[key] = decorateRefBaseName(maybeRef, namespace);
}
}
if ('security' in node && Array.isArray(node.security)) {
for (const securityRequirements of node.security) {
prefixObjectKeys(securityRequirements);

View file

@ -0,0 +1,11 @@
/*
* 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 function isLocalRef(ref: string): boolean {
return ref.startsWith('#/');
}

View file

@ -22,9 +22,6 @@ export interface MergerConfig {
outputFilePath: string;
options?: {
mergedSpecInfo?: Partial<OpenAPIV3.InfoObject>;
conflictsResolution?: {
prependComponentsWith: 'title';
};
};
}

View file

@ -45,5 +45,5 @@ function getVersionedOutputFilePath(outputFilePath: string, version: string): st
const filename = basename(outputFilePath);
return outputFilePath.replace(filename, `${version}-${filename}`);
return outputFilePath.replace(filename, `${version}_${filename}`);
}

View file

@ -708,4 +708,89 @@ describe('OpenAPI Merger - merging specs with conflicting components', () => {
Spec2_SomeCallback: expect.anything(),
});
});
it('prefixes discriminator mapping local references', async () => {
const spec1 = createOASDocument({
info: {
title: 'Spec1',
version: '2023-10-31',
},
paths: {
'/api/some_api': {
get: {
responses: {
'200': {
description: 'Successful response',
content: {
'application/json': {
schema: {
oneOf: [
{ $ref: '#/components/schemas/Component1' },
{ $ref: '#/components/schemas/Component2' },
],
discriminator: {
propertyName: 'commonProp',
mapping: {
component1: '#/components/schemas/Component1',
},
},
},
},
},
},
},
},
},
},
components: {
schemas: {
Component1: {
type: 'object',
properties: {
commonProp: {
type: 'string',
},
extraProp1: {
type: 'boolean',
},
},
},
Component2: {
type: 'object',
properties: {
commonProp: {
type: 'string',
},
extraProp2: {
type: 'integer',
},
},
},
},
},
});
const [mergedSpec] = Object.values(
await mergeSpecs({
1: spec1,
})
);
expect(mergedSpec.paths['/api/some_api']?.get?.responses['200']).toMatchObject({
content: {
'application/json; Elastic-Api-Version=2023-10-31': {
schema: expect.objectContaining({
discriminator: expect.objectContaining({
mapping: {
component1: '#/components/schemas/Spec1_Component1',
},
}),
}),
},
},
});
expect(mergedSpec.components?.schemas).toMatchObject({
Spec1_Component1: expect.anything(),
});
});
});

View file

@ -11,34 +11,36 @@ const { join, resolve } = require('path');
const ROOT = resolve(__dirname, '../..');
bundle({
sourceGlob: join(ROOT, 'common/api/detection_engine/**/*.schema.yaml'),
outputFilePath: join(
ROOT,
'docs/openapi/serverless/security_solution_detections_api_{version}.bundled.schema.yaml'
),
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.',
(async () => {
await bundle({
sourceGlob: join(ROOT, 'common/api/detection_engine/**/*.schema.yaml'),
outputFilePath: join(
ROOT,
'docs/openapi/serverless/security_solution_detections_api_{version}.bundled.schema.yaml'
),
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.',
},
},
},
});
});
bundle({
sourceGlob: join(ROOT, 'common/api/detection_engine/**/*.schema.yaml'),
outputFilePath: join(
ROOT,
'docs/openapi/ess/security_solution_detections_api_{version}.bundled.schema.yaml'
),
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.',
await bundle({
sourceGlob: join(ROOT, 'common/api/detection_engine/**/*.schema.yaml'),
outputFilePath: join(
ROOT,
'docs/openapi/ess/security_solution_detections_api_{version}.bundled.schema.yaml'
),
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.',
},
},
},
});
});
})();