Skip to content

Commit

Permalink
feat(ibm-schema-keywords): add new validation rule
Browse files Browse the repository at this point in the history
This commit introduces the new 'ibm-schema-keywords' validation
rule which will verify that each schema and schema property
are defined with only "allowed" keywords (fields), whwere the set
of allowable keywords are defined in a configurable allow list.

Signed-off-by: Phil Adams <[email protected]>
  • Loading branch information
padamstx committed Sep 27, 2023
1 parent e56d4fd commit f6f278e
Show file tree
Hide file tree
Showing 13 changed files with 497 additions and 28 deletions.
157 changes: 157 additions & 0 deletions docs/ibm-cloud-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ which is delivered in the `@ibm-cloud/openapi-ruleset` NPM package.
* [ibm-resource-response-consistency](#ibm-resource-response-consistency)
* [ibm-response-status-codes](#ibm-response-status-codes)
* [ibm-schema-description](#ibm-schema-description)
* [ibm-schema-keywords](#ibm-schema-keywords)
* [ibm-schema-type](#ibm-schema-type)
* [ibm-schema-type-format](#ibm-schema-type-format)
* [ibm-sdk-operations](#ibm-sdk-operations)
Expand Down Expand Up @@ -510,6 +511,13 @@ has non-form content.</td>
<td>oas3</td>
</tr>
<tr>
<td><a href="#ibm-schema-keywords">ibm-schema-keywords</a></td>
<td>error</td>
<td>Verifies that schemas and schema properties in an OpenAPI 3.1 document are defined using only
specific "allow-listed" keywords.</td>
<td>oas3_1</td>
</tr>
<tr>
<td><a href="#ibm-schema-type">ibm-schema-type</a></td>
<td>off</td>
<td>Schemas and schema properties should have a non-empty <code>type</code> field. <b>This rule is disabled by default.</b></td>
Expand Down Expand Up @@ -5336,6 +5344,155 @@ components:
</table>


### ibm-schema-keywords
<table>
<tr>
<td><b>Rule id:</b></td>
<td><b>ibm-schema-keywords</b></td>
</tr>
<tr>
<td valign=top><b>Description:</b></td>
<td>
This rule verifies that only certain keywords (fields) are used when defining schemas and schema properties
in an OpenAPI 3.1.x document. The allowable keywords are configurable (see the <code>Configuration</code> section below).
</td>
</tr>
<tr>
<td><b>Severity:</b></td>
<td>error</td>
</tr>
<tr>
<td><b>OAS Versions:</b></td>
<td>oas3_1</td>
</tr>
<tr>
<td valign=top><b>Configuration:</b></td>
<td>This rule supports a configuration object that specifies the set of keywords that are allowed within a schema
or schema property.
<p>The default configuration object provided with the rule is:
<pre>
{
keywordAllowList: [
'$ref',
'additionalProperties',
'allOf',
'anyOf',
'default',
'description',
'discriminator',
'enum',
'example',
'exclusiveMaximum',
'exclusiveMinimum',
'format',
'items',
'maximum',
'maxItems',
'maxLength',
'maxProperties',
'minimum',
'minItems',
'minLength',
'minProperties',
'multipleOf',
'not',
'oneOf',
'pattern',
'patternProperties',
'properties',
'readOnly',
'required',
'title',
'type',
'uniqueItems',
'unevaluatedProperties',
'writeOnly',
]
}
</pre>
<p>To configure the rule with a different set of allowable keywords, you'll need to
<a href="#replace-a-rule-from-ibm-cloudopenapi-ruleset">replace this rule with a new rule within your
custom ruleset</a> and modify the configuration such that the value of the <code>keywordAllowList</code> field
contains the desired set of keywords to be checked.
For example, to configure the rule so that <code>uniqueItems</code> and <code>unevaluatedProperties</code> are disallowed,
modify the configuration to remove these keywords from the <code>keywordAllowList</code>
configuration field, like this:
<pre>
{
keywordAllowList: [
'$ref',
'additionalProperties',
'allOf',
'anyOf',
'default',
'description',
'discriminator',
'enum',
'example',
'exclusiveMaximum',
'exclusiveMinimum',
'format',
'items',
'maximum',
'maxItems',
'maxLength',
'maxProperties',
'minimum',
'minItems',
'minLength',
'minProperties',
'multipleOf',
'not',
'oneOf',
'pattern',
'patternProperties',
'properties',
'readOnly',
'required',
'title',
'type',
'writeOnly',
]
}
</pre>
</td>
</tr>
<tr>
<td valign=top><b>Non-compliant example:<b></td>
<td>
<pre>
components:
schemas:
Things:
type: object
properties:
thing_id:
type: string
$comment: A comment about this property definition
nullable: true
</pre>
</td>
</tr>
<tr>
<td valign=top><b>Compliant example:</b></td>
<td>
<pre>
components:
schemas:
Things:
type: object
properties:
thing_id:
description: A comment about this property definition
type:
- string
- null
</pre>
</td>
</tr>
</table>


### ibm-schema-type
<table>
<tr>
Expand Down
67 changes: 67 additions & 0 deletions packages/ruleset/src/functions/allowed-keywords.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Copyright 2023 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

const { validateSubschemas } = require('@ibm-cloud/openapi-ruleset-utilities');
const { LoggerFactory } = require('../utils');

let ruleId;
let logger;

module.exports = function (obj, options, context) {
if (!logger) {
ruleId = context.rule.name;
logger = LoggerFactory.getInstance().getLogger(ruleId);
}
return validateSubschemas(
obj,
context.path,
(schema, path) => allowedKeywords(schema, path, options),
true,
true
);
};

/**
* This function will check to make sure that 'obj' is an object that contains only fields (keys)
* that are contained in the configured allow-list or extensions ('x-*').
* @param {*} obj the object within the OpenAPI document to check for allowed keywords
* @param {*} path the location of 'obj' within the OpenAPI document
* @param {*} options this is the value of the 'functionOptions' field within this rule's definition.
* This should be an object with the following fields:
* - 'keywordAllowList': an array of strings which are the allowed keywords
*
* @returns an array containing zero or more error objects
*/
function allowedKeywords(obj, path, options) {
logger.debug(
`${ruleId}: checking for allowed keywords in object located at: ${path.join(
'.'
)}`
);

// Find the fields of 'obj' that are not an extension or an allowed keyword.
const disallowedKeywords = Object.keys(obj).filter(
k => !(k.startsWith('x-') || options.keywordAllowList.includes(k))
);

// Return an error for each disallowed keyword that we found.
if (disallowedKeywords.length) {
logger.debug(
`${ruleId}: found these disallowed keywords: ${JSON.stringify(
disallowedKeywords
)}`
);

return disallowedKeywords.map(k => {
return {
message: `Found disallowed keyword: ${k}`,
path: [...path, k],
};
});
}

logger.debug(`PASSED!`);
return [];
}
1 change: 1 addition & 0 deletions packages/ruleset/src/functions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

module.exports = {
allowedKeywords: require('./allowed-keywords'),
arrayAttributes: require('./array-attributes'),
arrayOfArrays: require('./array-of-arrays'),
arrayResponses: require('./array-responses'),
Expand Down
2 changes: 1 addition & 1 deletion packages/ruleset/src/functions/no-operation-requestbody.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ module.exports = function (operation, options, context) {
* This function will check to make sure that certain operations do not have a requestBody.
* @param {*} operation the operation object to check
* @param {*} path the location of 'operation' within the OpenAPI document
* @param {*} config this is the value of the 'functionOptions' field
* @param {*} options this is the value of the 'functionOptions' field
* within this rule's definition (see src/rules/operation-requestbody.js).
* This should be an object with the following fields:
* - 'httpMethods': an array of strings which are the http methods that should be checked
Expand Down
1 change: 1 addition & 0 deletions packages/ruleset/src/ibm-oas.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ module.exports = {
'ibm-resource-response-consistency': ibmRules.resourceResponseConsistency,
'ibm-response-status-codes': ibmRules.responseStatusCodes,
'ibm-schema-description': ibmRules.schemaDescriptionExists,
'ibm-schema-keywords': ibmRules.schemaKeywords,
'ibm-schema-type': ibmRules.schemaTypeExists,
'ibm-schema-type-format': ibmRules.schemaTypeFormat,
'ibm-sdk-operations': ibmRules.ibmSdkOperations,
Expand Down
1 change: 1 addition & 0 deletions packages/ruleset/src/rules/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ module.exports = {
responseExampleExists: require('./response-example-exists'),
responseStatusCodes: require('./response-status-codes'),
schemaDescriptionExists: require('./schema-description-exists'),
schemaKeywords: require('./schema-keywords'),
schemaTypeExists: require('./schema-type-exists'),
schemaTypeFormat: require('./schema-type-format'),
securitySchemes: require('./securityschemes'),
Expand Down
61 changes: 61 additions & 0 deletions packages/ruleset/src/rules/schema-keywords.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright 2023 IBM Corporation.
* SPDX-License-Identifier: Apache2.0
*/

const {
schemas,
} = require('@ibm-cloud/openapi-ruleset-utilities/src/collections');
const { oas3_1 } = require('@stoplight/spectral-formats');
const { allowedKeywords } = require('../functions');

module.exports = {
description:
'Verifies that schema objects include only allowed-listed keywords',
message: '{{error}}',
severity: 'error',
formats: [oas3_1],
resolved: true,
given: schemas,
then: {
function: allowedKeywords,
functionOptions: {
keywordAllowList: [
'$ref',
'additionalProperties',
'allOf',
'anyOf',
'default',
'description',
'discriminator',
'enum',
'example',
'exclusiveMaximum',
'exclusiveMinimum',
'format',
'items',
'maximum',
'maxItems',
'maxLength',
'maxProperties',
'minimum',
'minItems',
'minLength',
'minProperties',
'multipleOf',
'not',
'oneOf',
'pattern',
'patternProperties',
'properties',
'readOnly',
'required',
'title',
'type',
'uniqueItems',
'unevaluatedProperties',
'writeOnly',
],
},
},
};
4 changes: 3 additions & 1 deletion packages/ruleset/test/no-nullable-properties.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const expectedMsg =
describe(`Spectral rule: ${ruleId}`, () => {
describe('Should not yield errors', () => {
it('Clean spec', async () => {
// rootDocument contains a nullable property in the CarPatch schema.
const testDocument = makeCopy(rootDocument);
testDocument.components.schemas.CarPatch.properties.make.nullable = true;

const results = await testRule(ruleId, rule, rootDocument);
expect(results).toHaveLength(0);
});
Expand Down
Loading

0 comments on commit f6f278e

Please sign in to comment.