Skip to content

Commit 8c5de56

Browse files
IPA-118: Extensible by Default
1 parent a07466e commit 8c5de56

File tree

7 files changed

+458
-0
lines changed

7 files changed

+458
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import testRule from './__helpers__/testRule';
2+
import { DiagnosticSeverity } from '@stoplight/types';
3+
4+
testRule('xgen-IPA-118-no-additional-properties-false', [
5+
{
6+
name: 'valid without additionalProperties',
7+
document: {
8+
components: {
9+
schemas: {
10+
ExampleSchema: {
11+
type: 'object',
12+
properties: {
13+
name: { type: 'string' },
14+
value: { type: 'integer' },
15+
},
16+
},
17+
},
18+
},
19+
},
20+
errors: [],
21+
},
22+
{
23+
name: 'valid with additionalProperties: true',
24+
document: {
25+
components: {
26+
schemas: {
27+
ExampleSchema: {
28+
type: 'object',
29+
properties: {
30+
name: { type: 'string' },
31+
},
32+
additionalProperties: true,
33+
},
34+
},
35+
},
36+
},
37+
errors: [],
38+
},
39+
{
40+
name: 'valid with additionalProperties as schema',
41+
document: {
42+
components: {
43+
schemas: {
44+
ExampleSchema: {
45+
type: 'object',
46+
properties: {
47+
name: { type: 'string' },
48+
},
49+
additionalProperties: {
50+
type: 'string',
51+
},
52+
},
53+
},
54+
},
55+
},
56+
errors: [],
57+
},
58+
{
59+
name: 'invalid with additionalProperties: false',
60+
document: {
61+
openapi: '3.0.0',
62+
components: {
63+
schemas: {
64+
ExampleSchema: {
65+
type: 'object',
66+
properties: {
67+
name: { type: 'string' },
68+
},
69+
additionalProperties: false,
70+
},
71+
ReferencedSchema: {
72+
type: 'object',
73+
properties: {
74+
property: { $ref: '#/components/schemas/ExampleSchema' },
75+
},
76+
},
77+
},
78+
},
79+
},
80+
errors: [
81+
{
82+
code: 'xgen-IPA-118-no-additional-properties-false',
83+
message:
84+
"Schema must not use 'additionalProperties: false'. Consider using 'additionalProperties: true' or omitting the property.",
85+
path: ['components', 'schemas', 'ExampleSchema'],
86+
severity: DiagnosticSeverity.Warning,
87+
},
88+
],
89+
},
90+
{
91+
name: 'invalid with nested additionalProperties: false',
92+
document: {
93+
components: {
94+
schemas: {
95+
ParentSchema: {
96+
type: 'object',
97+
properties: {
98+
child: {
99+
type: 'object',
100+
properties: {
101+
name: { type: 'string' },
102+
},
103+
additionalProperties: false,
104+
},
105+
},
106+
},
107+
},
108+
},
109+
},
110+
errors: [
111+
{
112+
code: 'xgen-IPA-118-no-additional-properties-false',
113+
message:
114+
"Schema must not use 'additionalProperties: false'. Consider using 'additionalProperties: true' or omitting the property.",
115+
path: ['components', 'schemas', 'ParentSchema', 'properties', 'child'],
116+
severity: DiagnosticSeverity.Warning,
117+
},
118+
],
119+
},
120+
{
121+
name: 'with exception tag',
122+
document: {
123+
components: {
124+
schemas: {
125+
ExampleSchema: {
126+
type: 'object',
127+
'x-xgen-IPA-exception': {
128+
'xgen-IPA-118-no-additional-properties-false': 'Exception reason',
129+
},
130+
properties: {
131+
name: { type: 'string' },
132+
},
133+
additionalProperties: false,
134+
},
135+
ParentSchema: {
136+
type: 'object',
137+
'x-xgen-IPA-exception': {
138+
'xgen-IPA-118-no-additional-properties-false': 'Exception reason',
139+
},
140+
properties: {
141+
child: {
142+
type: 'object',
143+
properties: {
144+
name: { type: 'string' },
145+
},
146+
additionalProperties: false,
147+
},
148+
},
149+
},
150+
},
151+
},
152+
},
153+
errors: [],
154+
},
155+
]);

tools/spectral/ipa/__tests__/utils/compareUtils.test.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it } from '@jest/globals';
22
import {
3+
findAdditionalPropertiesFalsePaths,
34
isDeepEqual,
45
removePropertiesByFlag,
56
removePropertyKeys,
@@ -329,3 +330,157 @@ describe('removeRequestProperties', () => {
329330
expect(removeRequestProperties(input)).toEqual(expected);
330331
});
331332
});
333+
334+
describe('findAdditionalPropertiesFalsePaths', () => {
335+
it('finds additionalProperties:false at root level', () => {
336+
const schema = {
337+
type: 'object',
338+
properties: {
339+
name: { type: 'string' },
340+
},
341+
additionalProperties: false,
342+
};
343+
344+
const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
345+
expect(results).toEqual([['root']]);
346+
});
347+
348+
it('finds additionalProperties:false in nested properties', () => {
349+
const schema = {
350+
type: 'object',
351+
properties: {
352+
user: {
353+
type: 'object',
354+
properties: {
355+
address: {
356+
type: 'object',
357+
additionalProperties: false,
358+
},
359+
},
360+
},
361+
},
362+
};
363+
364+
const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
365+
expect(results).toEqual([['root', 'properties', 'user', 'properties', 'address']]);
366+
});
367+
368+
it('finds additionalProperties:false in array items', () => {
369+
const schema = {
370+
type: 'array',
371+
items: {
372+
type: 'object',
373+
additionalProperties: false,
374+
},
375+
};
376+
377+
const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
378+
expect(results).toEqual([['root', 'items']]);
379+
});
380+
381+
it('finds additionalProperties:false in composition keywords', () => {
382+
const schema = {
383+
oneOf: [
384+
{
385+
type: 'object',
386+
additionalProperties: false,
387+
},
388+
{
389+
type: 'object',
390+
properties: {
391+
name: { type: 'string' },
392+
},
393+
},
394+
],
395+
};
396+
397+
const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
398+
expect(results).toEqual([['root', 'oneOf', '0']]);
399+
});
400+
401+
it('finds multiple additionalProperties:false occurrences', () => {
402+
const schema = {
403+
type: 'object',
404+
additionalProperties: false,
405+
properties: {
406+
user: {
407+
type: 'object',
408+
additionalProperties: false,
409+
properties: {
410+
addresses: {
411+
type: 'array',
412+
items: {
413+
type: 'object',
414+
additionalProperties: false,
415+
},
416+
},
417+
},
418+
},
419+
metadata: {
420+
allOf: [
421+
{
422+
type: 'object',
423+
additionalProperties: false,
424+
},
425+
],
426+
},
427+
},
428+
};
429+
430+
const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
431+
expect(results).toEqual([
432+
['root'],
433+
['root', 'properties', 'user'],
434+
['root', 'properties', 'user', 'properties', 'addresses', 'items'],
435+
['root', 'properties', 'metadata', 'allOf', '0'],
436+
]);
437+
});
438+
439+
it('handles deeply nested structures', () => {
440+
const schema = {
441+
type: 'object',
442+
properties: {
443+
level1: {
444+
type: 'object',
445+
properties: {
446+
level2: {
447+
type: 'object',
448+
properties: {
449+
level3: {
450+
type: 'object',
451+
properties: {
452+
level4: {
453+
type: 'object',
454+
additionalProperties: false,
455+
},
456+
},
457+
},
458+
},
459+
},
460+
},
461+
},
462+
},
463+
};
464+
465+
const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
466+
expect(results).toEqual([
467+
['root', 'properties', 'level1', 'properties', 'level2', 'properties', 'level3', 'properties', 'level4'],
468+
]);
469+
});
470+
471+
it('does not find additionalProperties:true or schema objects', () => {
472+
const schema = {
473+
type: 'object',
474+
additionalProperties: true,
475+
properties: {
476+
user: {
477+
type: 'object',
478+
additionalProperties: { type: 'string' },
479+
},
480+
},
481+
};
482+
483+
const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
484+
expect(results).toEqual([]);
485+
});
486+
});

tools/spectral/ipa/ipa-spectral.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ extends:
1212
- ./rulesets/IPA-113.yaml
1313
- ./rulesets/IPA-114.yaml
1414
- ./rulesets/IPA-117.yaml
15+
- ./rulesets/IPA-118.yaml
1516
- ./rulesets/IPA-123.yaml
1617
- ./rulesets/IPA-125.yaml
1718

@@ -41,3 +42,15 @@ overrides:
4142
- '**#/components/schemas/UserSecurity/properties/customerX509' # unable to document exceptions, to be covered by CLOUDP-308286
4243
rules:
4344
xgen-IPA-112-field-names-are-camel-case: 'off'
45+
- files:
46+
- '**#/components/schemas/DataLakeS3StoreSettings/allOf/1/properties/additionalStorageClasses' # unable to document exceptions, to be covered by CLOUDP-293178
47+
- '**#/components/schemas/DataLakeDatabaseDataSourceSettings/properties/databaseRegex' # unable to document exceptions, to be covered by CLOUDP-293178
48+
- '**#/components/schemas/DataLakeDatabaseDataSourceSettings/properties/collectionRegex' # unable to document exceptions, to be covered by CLOUDP-293178
49+
rules:
50+
xgen-IPA-117-description-should-not-use-inline-links: 'off'
51+
- files:
52+
- '**#/paths/~1api~1atlas~1v2~1unauth~1openapi~1versions' # external reference, to be covered by CLOUDP-309694
53+
- '**#/paths/~1api~1atlas~1v2~1openapi~1info' # external reference, to be covered by CLOUDP-309694
54+
- '**#/paths/~1rest~1unauth~1version' # external reference, to be covered by CLOUDP-309694
55+
rules:
56+
xgen-IPA-114-error-responses-refer-to-api-error: 'off'
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# IPA-118: Extensible by Default
2+
# http://go/ipa/118
3+
4+
functions:
5+
- IPA118NoAdditionalPropertiesFalse
6+
7+
rules:
8+
xgen-IPA-118-no-additional-properties-false:
9+
description: |
10+
Schemas must not use `additionalProperties: false`
11+
12+
##### Implementation details
13+
This rule checks that schemas don't restrict additional properties by setting `additionalProperties: false`.
14+
Schemas without explicit `additionalProperties` settings (which default to true) or with `additionalProperties` set to `true` are compliant.
15+
This rule checks all nested schemas, but only parent schemas can be marked for exception.
16+
message: '{{error}} https://mdb.link/mongodb-atlas-openapi-validation#xgen-IPA-118-no-additional-properties-false'
17+
severity: warn
18+
given: '$.components.schemas[*]'
19+
then:
20+
function: 'IPA118NoAdditionalPropertiesFalse'

tools/spectral/ipa/rulesets/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,22 @@ The rule checks for the presence of the `schema`, `examples` or `example` proper
772772

773773

774774

775+
### IPA-118
776+
777+
Rules are based on [http://go/ipa/IPA-118](http://go/ipa/IPA-118).
778+
779+
#### xgen-IPA-118-no-additional-properties-false
780+
781+
![warn](https://img.shields.io/badge/warning-yellow)
782+
Schemas must not use `additionalProperties: false`
783+
784+
##### Implementation details
785+
This rule checks that schemas don't restrict additional properties by setting `additionalProperties: false`.
786+
Schemas without explicit `additionalProperties` settings (which default to true) or with `additionalProperties` set to `true` are compliant.
787+
This rule checks all nested schemas, but only parent schemas can be marked for exception.
788+
789+
790+
775791
### IPA-123
776792

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

0 commit comments

Comments
 (0)