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 2046871f7..aa49f3acd 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 @@ -210,4 +210,33 @@ testRule('oas3-operation-security-defined', [ }, ], }, + + { + name: 'oas3.1: bearer http scopes on operation without scheme-level scopes', + document: { + openapi: '3.1.0', + info: { title: 'test', version: '1.0.0' }, + paths: { + '/users': { + get: { + security: [ + { + bearerAuth: ['read:users', 'public'], + }, + ], + }, + }, + }, + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'jwt', + }, + }, + }, + }, + errors: [], + }, ]); diff --git a/packages/rulesets/src/oas/functions/oasSecurityDefined.ts b/packages/rulesets/src/oas/functions/oasSecurityDefined.ts index 1043a9bc2..20dbbc151 100644 --- a/packages/rulesets/src/oas/functions/oasSecurityDefined.ts +++ b/packages/rulesets/src/oas/functions/oasSecurityDefined.ts @@ -33,6 +33,9 @@ export default createRulesetFunction, Options>( if (!isPlainObject(document.data)) return; + const openapiVersion = + typeof document.data.openapi === 'string' ? document.data.openapi : ''; + const allDefs = oasVersion === 2 ? document.data.securityDefinitions @@ -58,7 +61,7 @@ export default createRulesetFunction, Options>( const scope = input[schemeName]; for (let i = 0; i < scope.length; i++) { const scopeName = scope[i]; - if (!isScopeDefined(oasVersion, scopeName, allDefs[schemeName])) { + if (!isScopeDefined(oasVersion, scopeName, allDefs[schemeName], openapiVersion)) { results ??= []; results.push({ message: `"${scopeName}" must be listed among scopes.`, @@ -72,9 +75,24 @@ export default createRulesetFunction, Options>( }, ); -function isScopeDefined(oasVersion: 2 | 3, scopeName: string, securityScheme: unknown): boolean { +function isScopeDefined( + oasVersion: 2 | 3, + scopeName: string, + securityScheme: unknown, + openapiVersion = '', +): boolean { if (!isPlainObject(securityScheme)) return false; + // OpenAPI 3.1 allows scope lists on http bearer requirements without scheme-level scope definitions + if ( + oasVersion === 3 && + openapiVersion.startsWith('3.1') && + securityScheme.type === 'http' && + securityScheme.scheme === 'bearer' + ) { + return true; + } + if (oasVersion === 2) { return isPlainObject(securityScheme.scopes) && scopeName in securityScheme.scopes; }