Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -0,0 +1,155 @@
import testRule from './__helpers__/testRule';
import { DiagnosticSeverity } from '@stoplight/types';

testRule('xgen-IPA-118-no-additional-properties-false', [
{
name: 'valid without additionalProperties',
document: {
components: {
schemas: {
ExampleSchema: {
type: 'object',
properties: {
name: { type: 'string' },
value: { type: 'integer' },
},
},
},
},
},
errors: [],
},
{
name: 'valid with additionalProperties: true',
document: {
components: {
schemas: {
ExampleSchema: {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: true,
},
},
},
},
errors: [],
},
{
name: 'valid with additionalProperties as schema',
document: {
components: {
schemas: {
ExampleSchema: {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: {
type: 'string',
},
},
},
},
},
errors: [],
},
{
name: 'invalid with additionalProperties: false',
document: {
openapi: '3.0.0',
components: {
schemas: {
ExampleSchema: {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: false,
},
ReferencedSchema: {
type: 'object',
properties: {
property: { $ref: '#/components/schemas/ExampleSchema' },
},
},
},
},
},
errors: [
{
code: 'xgen-IPA-118-no-additional-properties-false',
message:
"Schema must not use 'additionalProperties: false'. Consider using 'additionalProperties: true' or omitting the property.",
path: ['components', 'schemas', 'ExampleSchema'],
severity: DiagnosticSeverity.Warning,
},
],
},
{
name: 'invalid with nested additionalProperties: false',
document: {
components: {
schemas: {
ParentSchema: {
type: 'object',
properties: {
child: {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: false,
},
},
},
},
},
},
errors: [
{
code: 'xgen-IPA-118-no-additional-properties-false',
message:
"Schema must not use 'additionalProperties: false'. Consider using 'additionalProperties: true' or omitting the property.",
path: ['components', 'schemas', 'ParentSchema', 'properties', 'child'],
severity: DiagnosticSeverity.Warning,
},
],
},
{
name: 'with exception tag',
document: {
components: {
schemas: {
ExampleSchema: {
type: 'object',
'x-xgen-IPA-exception': {
'xgen-IPA-118-no-additional-properties-false': 'Exception reason',
},
properties: {
name: { type: 'string' },
},
additionalProperties: false,
},
ParentSchema: {
type: 'object',
'x-xgen-IPA-exception': {
'xgen-IPA-118-no-additional-properties-false': 'Exception reason',
},
properties: {
child: {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: false,
},
},
},
},
},
},
errors: [],
},
]);
155 changes: 155 additions & 0 deletions tools/spectral/ipa/__tests__/utils/compareUtils.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from '@jest/globals';
import {
findAdditionalPropertiesFalsePaths,
isDeepEqual,
removePropertiesByFlag,
removePropertyKeys,
Expand Down Expand Up @@ -329,3 +330,157 @@ describe('removeRequestProperties', () => {
expect(removeRequestProperties(input)).toEqual(expected);
});
});

describe('findAdditionalPropertiesFalsePaths', () => {
it('finds additionalProperties:false at root level', () => {
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: false,
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([['root']]);
});

it('finds additionalProperties:false in nested properties', () => {
const schema = {
type: 'object',
properties: {
user: {
type: 'object',
properties: {
address: {
type: 'object',
additionalProperties: false,
},
},
},
},
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([['root', 'properties', 'user', 'properties', 'address']]);
});

it('finds additionalProperties:false in array items', () => {
const schema = {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
},
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([['root', 'items']]);
});

it('finds additionalProperties:false in composition keywords', () => {
const schema = {
oneOf: [
{
type: 'object',
additionalProperties: false,
},
{
type: 'object',
properties: {
name: { type: 'string' },
},
},
],
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([['root', 'oneOf', '0']]);
});

it('finds multiple additionalProperties:false occurrences', () => {
const schema = {
type: 'object',
additionalProperties: false,
properties: {
user: {
type: 'object',
additionalProperties: false,
properties: {
addresses: {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
},
},
},
},
metadata: {
allOf: [
{
type: 'object',
additionalProperties: false,
},
],
},
},
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([
['root'],
['root', 'properties', 'user'],
['root', 'properties', 'user', 'properties', 'addresses', 'items'],
['root', 'properties', 'metadata', 'allOf', '0'],
]);
});

it('handles deeply nested structures', () => {
const schema = {
type: 'object',
properties: {
level1: {
type: 'object',
properties: {
level2: {
type: 'object',
properties: {
level3: {
type: 'object',
properties: {
level4: {
type: 'object',
additionalProperties: false,
},
},
},
},
},
},
},
},
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([
['root', 'properties', 'level1', 'properties', 'level2', 'properties', 'level3', 'properties', 'level4'],
]);
});

it('does not find additionalProperties:true or schema objects', () => {
const schema = {
type: 'object',
additionalProperties: true,
properties: {
user: {
type: 'object',
additionalProperties: { type: 'string' },
},
},
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([]);
});
});
13 changes: 13 additions & 0 deletions tools/spectral/ipa/ipa-spectral.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ extends:
- ./rulesets/IPA-113.yaml
- ./rulesets/IPA-114.yaml
- ./rulesets/IPA-117.yaml
- ./rulesets/IPA-118.yaml
- ./rulesets/IPA-123.yaml
- ./rulesets/IPA-125.yaml

Expand Down Expand Up @@ -41,3 +42,15 @@ overrides:
- '**#/components/schemas/UserSecurity/properties/customerX509' # unable to document exceptions, to be covered by CLOUDP-308286
rules:
xgen-IPA-112-field-names-are-camel-case: 'off'
- files:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI drive-by fix to sync with MMS

- '**#/components/schemas/DataLakeS3StoreSettings/allOf/1/properties/additionalStorageClasses' # unable to document exceptions, to be covered by CLOUDP-293178
- '**#/components/schemas/DataLakeDatabaseDataSourceSettings/properties/databaseRegex' # unable to document exceptions, to be covered by CLOUDP-293178
- '**#/components/schemas/DataLakeDatabaseDataSourceSettings/properties/collectionRegex' # unable to document exceptions, to be covered by CLOUDP-293178
rules:
xgen-IPA-117-description-should-not-use-inline-links: 'off'
- files:
- '**#/paths/~1api~1atlas~1v2~1unauth~1openapi~1versions' # external reference, to be covered by CLOUDP-309694
- '**#/paths/~1api~1atlas~1v2~1openapi~1info' # external reference, to be covered by CLOUDP-309694
- '**#/paths/~1rest~1unauth~1version' # external reference, to be covered by CLOUDP-309694
rules:
xgen-IPA-114-error-responses-refer-to-api-error: 'off'
20 changes: 20 additions & 0 deletions tools/spectral/ipa/rulesets/IPA-118.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# IPA-118: Extensible by Default
# http://go/ipa/118

functions:
- IPA118NoAdditionalPropertiesFalse

rules:
xgen-IPA-118-no-additional-properties-false:
description: |
Schemas must not use `additionalProperties: false`

##### Implementation details
This rule checks that schemas don't restrict additional properties by setting `additionalProperties: false`.
Schemas without explicit `additionalProperties` settings (which default to true) or with `additionalProperties` set to `true` are compliant.
This rule checks all nested schemas, but only parent schemas can be marked for exception.
message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-118-no-additional-properties-false'
severity: warn
given: '$.components.schemas[*]'
then:
function: 'IPA118NoAdditionalPropertiesFalse'
16 changes: 16 additions & 0 deletions tools/spectral/ipa/rulesets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,22 @@ The rule checks for the presence of the `schema`, `examples` or `example` proper



### IPA-118

Rules are based on [http://go/ipa/IPA-118](http://go/ipa/IPA-118).

#### xgen-IPA-118-no-additional-properties-false

![warn](https://img.shields.io/badge/warning-yellow)
Schemas must not use `additionalProperties: false`

##### Implementation details
This rule checks that schemas don't restrict additional properties by setting `additionalProperties: false`.
Schemas without explicit `additionalProperties` settings (which default to true) or with `additionalProperties` set to `true` are compliant.
This rule checks all nested schemas, but only parent schemas can be marked for exception.



### IPA-123

Rules are based on [http://go/ipa/IPA-123](http://go/ipa/IPA-123).
Expand Down
Loading
Loading