From 590eb017975866d1f24a22271bf6801d132335ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Thu, 14 Sep 2023 16:44:53 +0200 Subject: [PATCH 1/3] refactor(rulesets): simplify oasOpSecurityDefined --- packages/rulesets/src/oas/functions/index.ts | 4 +- .../src/oas/functions/oasOpSecurityDefined.ts | 110 ------------------ .../src/oas/functions/oasSecurityDefined.ts | 53 +++++++++ packages/rulesets/src/oas/index.ts | 15 +-- 4 files changed, 63 insertions(+), 119 deletions(-) delete mode 100644 packages/rulesets/src/oas/functions/oasOpSecurityDefined.ts create mode 100644 packages/rulesets/src/oas/functions/oasSecurityDefined.ts diff --git a/packages/rulesets/src/oas/functions/index.ts b/packages/rulesets/src/oas/functions/index.ts index 430d8d9a8..debc64538 100644 --- a/packages/rulesets/src/oas/functions/index.ts +++ b/packages/rulesets/src/oas/functions/index.ts @@ -4,7 +4,7 @@ import { default as oasDocumentSchema } from './oasDocumentSchema'; import { default as oasOpFormDataConsumeCheck } from './oasOpFormDataConsumeCheck'; import { default as oasOpSuccessResponse } from './oasOpSuccessResponse'; import { default as oasExample } from './oasExample'; -import { default as oasOpSecurityDefined } from './oasOpSecurityDefined'; +import { default as oasSecurityDefined } from './oasSecurityDefined'; import { default as typedEnum } from './typedEnum'; import { default as refSiblings } from './refSiblings'; import { default as oasPathParam } from './oasPathParam'; @@ -20,7 +20,7 @@ export { oasOpFormDataConsumeCheck, oasOpSuccessResponse, oasExample, - oasOpSecurityDefined, + oasSecurityDefined, typedEnum, refSiblings, oasPathParam, diff --git a/packages/rulesets/src/oas/functions/oasOpSecurityDefined.ts b/packages/rulesets/src/oas/functions/oasOpSecurityDefined.ts deleted file mode 100644 index 37edd2eca..000000000 --- a/packages/rulesets/src/oas/functions/oasOpSecurityDefined.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { JsonPath } from '@stoplight/types'; -import { createRulesetFunction, IFunctionResult } from '@stoplight/spectral-core'; - -import { getAllOperations } from './utils/getAllOperations'; -import { isObject } from './utils/isObject'; - -function _get(value: unknown, path: JsonPath): unknown { - for (const segment of path) { - if (!isObject(value)) { - break; - } - - value = value[segment]; - } - - return value; -} - -type Options = { - schemesPath: JsonPath; -}; - -export default createRulesetFunction<{ paths: Record; security: unknown[] }, Options>( - { - input: { - type: 'object', - properties: { - paths: { - type: 'object', - }, - security: { - type: 'array', - }, - }, - }, - options: { - type: 'object', - properties: { - schemesPath: { - type: 'array', - items: { - type: ['string', 'number'], - }, - }, - }, - }, - }, - function oasOpSecurityDefined(targetVal, { schemesPath }) { - const { paths } = targetVal; - - const results: IFunctionResult[] = []; - - const schemes = _get(targetVal, schemesPath); - const allDefs = isObject(schemes) ? Object.keys(schemes) : []; - - // Check global security requirements - - const { security } = targetVal; - - if (Array.isArray(security)) { - for (const [index, value] of security.entries()) { - if (!isObject(value)) { - continue; - } - - const securityKeys = Object.keys(value); - - for (const securityKey of securityKeys) { - if (!allDefs.includes(securityKey)) { - results.push({ - message: `API "security" values must match a scheme defined in the "${schemesPath.join('.')}" object.`, - path: ['security', index, securityKey], - }); - } - } - } - } - - for (const { path, operation, value } of getAllOperations(paths)) { - if (!isObject(value)) continue; - - const { security } = value; - - if (!Array.isArray(security)) { - continue; - } - - for (const [index, value] of security.entries()) { - if (!isObject(value)) { - continue; - } - - const securityKeys = Object.keys(value); - - for (const securityKey of securityKeys) { - if (!allDefs.includes(securityKey)) { - results.push({ - message: `Operation "security" values must match a scheme defined in the "${schemesPath.join( - '.', - )}" object.`, - path: ['paths', path, operation, 'security', index, securityKey], - }); - } - } - } - } - - return results; - }, -); diff --git a/packages/rulesets/src/oas/functions/oasSecurityDefined.ts b/packages/rulesets/src/oas/functions/oasSecurityDefined.ts new file mode 100644 index 000000000..3e414659c --- /dev/null +++ b/packages/rulesets/src/oas/functions/oasSecurityDefined.ts @@ -0,0 +1,53 @@ +import { createRulesetFunction } from '@stoplight/spectral-core'; +import type { IFunctionResult } from '@stoplight/spectral-core'; +import { isPlainObject } from '@stoplight/json'; + +type Options = { + oasVersion: 2 | 3; +}; + +export default createRulesetFunction[], Options>( + { + input: { + type: 'object', + }, + options: { + type: 'object', + properties: { + oasVersion: { + enum: [2, 3], + }, + }, + additionalProperties: false, + }, + }, + function oasSecurityDefined(input, { oasVersion }, { document, path }) { + const schemeNames = Object.keys(input); + if (schemeNames.length === 0) return; + + if (!isPlainObject(document.data)) return; + + const allDefs = + oasVersion === 2 + ? document.data.securityDefinitions + : isPlainObject(document.data.components) + ? document.data.components.securitySchemes + : null; + + let results: IFunctionResult[] | undefined; + + for (const schemeName of schemeNames) { + if (!isPlainObject(allDefs) || !(schemeName in allDefs)) { + const scope = path.length == 2 ? 'API' : 'Operation'; + const location = oasVersion === 2 ? 'securityDefinitions' : 'components.securitySchemes'; + results ??= []; + results.push({ + message: `${scope} "security" values must match a scheme defined in the "${location}" object.`, + path: [...path, schemeName], + }); + } + } + + return results; + }, +); diff --git a/packages/rulesets/src/oas/index.ts b/packages/rulesets/src/oas/index.ts index 4f60d5e6f..f6f0e0516 100644 --- a/packages/rulesets/src/oas/index.ts +++ b/packages/rulesets/src/oas/index.ts @@ -21,7 +21,7 @@ import { oasExample, oasUnusedComponent, oasDocumentSchema, - oasOpSecurityDefined, + oasSecurityDefined, oasSchema, oasDiscriminator, } from './functions'; @@ -36,6 +36,7 @@ const ruleset = { aliases: { PathItem: ['$.paths[*]'], OperationObject: ['#PathItem[get,put,post,delete,options,head,patch,trace]'], + SecurityRequirementObject: ['$.security[*]', '#OperationObject.security[*]'], ResponseObject: { targets: [ { @@ -451,11 +452,11 @@ const ruleset = { message: '{{error}}', recommended: true, formats: [oas2], - given: '$', + given: '#SecurityRequirementObject', then: { - function: oasOpSecurityDefined, + function: oasSecurityDefined, functionOptions: { - schemesPath: ['securityDefinitions'], + oasVersion: 2, }, }, }, @@ -584,11 +585,11 @@ const ruleset = { message: '{{error}}', recommended: true, formats: [oas3], - given: '$', + given: '#SecurityRequirementObject', then: { - function: oasOpSecurityDefined, + function: oasSecurityDefined, functionOptions: { - schemesPath: ['components', 'securitySchemes'], + oasVersion: 3, }, }, }, From b5b68b32ad0e178d44b73775f63f3f8c76d5f603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Tue, 19 Sep 2023 20:48:02 +0200 Subject: [PATCH 2/3] test(rulesets): add empty security requirement --- .../src/oas/__tests__/oas3-operation-security-defined.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts b/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts index f3d7f5212..b024adc30 100644 --- a/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas3-operation-security-defined.test.ts @@ -116,6 +116,7 @@ testRule('oas3-operation-security-defined', [ apikey: [], basic: [], }, + {}, ], }, }, From f0e0144a829c3f1162c4a0c0fdadbcfd26c4f564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Tue, 19 Sep 2023 21:07:03 +0200 Subject: [PATCH 3/3] chore(rulesets): tweaks --- packages/rulesets/src/oas/functions/oasSecurityDefined.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/rulesets/src/oas/functions/oasSecurityDefined.ts b/packages/rulesets/src/oas/functions/oasSecurityDefined.ts index 3e414659c..bb273fd7d 100644 --- a/packages/rulesets/src/oas/functions/oasSecurityDefined.ts +++ b/packages/rulesets/src/oas/functions/oasSecurityDefined.ts @@ -6,7 +6,7 @@ type Options = { oasVersion: 2 | 3; }; -export default createRulesetFunction[], Options>( +export default createRulesetFunction, Options>( { input: { type: 'object', @@ -38,11 +38,11 @@ export default createRulesetFunction[], Options>( for (const schemeName of schemeNames) { if (!isPlainObject(allDefs) || !(schemeName in allDefs)) { - const scope = path.length == 2 ? 'API' : 'Operation'; + const object = path.length == 2 ? 'API' : 'Operation'; const location = oasVersion === 2 ? 'securityDefinitions' : 'components.securitySchemes'; results ??= []; results.push({ - message: `${scope} "security" values must match a scheme defined in the "${location}" object.`, + message: `${object} "security" values must match a scheme defined in the "${location}" object.`, path: [...path, schemeName], }); }