diff --git a/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts b/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts index 92d714ac5..3fd337d88 100644 --- a/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts +++ b/packages/rulesets/src/oas/__tests__/oas2-operation-security-defined.test.ts @@ -3,12 +3,21 @@ import testRule from './__helpers__/tester'; testRule('oas2-operation-security-defined', [ { - name: 'a correct object (just in body)', + name: 'valid case', document: { swagger: '2.0', securityDefinitions: { - apikey: {}, + apikey: { + type: 'apiKey', + name: 'api_key', + in: 'header', + }, }, + security: [ + { + apikey: [], + }, + ], paths: { '/path': { get: { @@ -25,37 +34,44 @@ testRule('oas2-operation-security-defined', [ }, { - name: 'a correct object (API-level security)', + name: 'valid and invalid object', document: { swagger: '2.0', securityDefinitions: { - apikey: {}, + apikey: { + type: 'apiKey', + name: 'api_key', + in: 'header', + }, + oauth2: { + type: 'oauth2', + flows: 'accessCode', + authorizationUrl: 'https://example.com/api/oauth/dialog', + tokenUrl: 'https://example.com/api/oauth/token', + scopes: { + 'write:pets': 'modify pets in your account', + 'read:pets': 'read your pets', + }, + }, }, security: [ { apikey: [], + basic: [], + oauth2: ['write:pets'], }, - ], - paths: { - '/path': { - get: {}, + {}, + { + oauth2: ['write:users', 'read:users'], }, - }, - }, - errors: [], - }, - - { - name: 'invalid object', - document: { - swagger: '2.0', - securityDefinitions: {}, + ], paths: { - '/path': { + '/users': { get: { security: [ { - apikey: [], + bearer: [], + oauth2: [], }, ], }, @@ -64,45 +80,32 @@ testRule('oas2-operation-security-defined', [ }, errors: [ { - message: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.', - path: ['paths', '/path', 'get', 'security', '0', 'apikey'], + message: 'API "security" values must match a scheme defined in the "securityDefinitions" object.', + path: ['security', '0', 'basic'], severity: DiagnosticSeverity.Warning, }, - ], - }, - - { - name: 'invalid object (API-level security)', - document: { - swagger: '2.0', - securityDefinitions: {}, - security: [ - { - apikey: [], - }, - ], - paths: { - '/path': { - get: {}, - }, + { + message: '"write:users" must be listed among scopes.', + path: ['security', '2', 'oauth2', '0'], + severity: DiagnosticSeverity.Warning, }, - }, - errors: [ { - message: 'API "security" values must match a scheme defined in the "securityDefinitions" object.', - path: ['security', '0', 'apikey'], + message: '"read:users" must be listed among scopes.', + path: ['security', '2', 'oauth2', '1'], + severity: DiagnosticSeverity.Warning, + }, + { + message: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.', + path: ['paths', '/users', 'get', 'security', '0', 'bearer'], severity: DiagnosticSeverity.Warning, }, ], }, { - name: 'valid and invalid object', + name: 'missing securityDefinitions', document: { swagger: '2.0', - securityDefinitions: { - apikey: {}, - }, paths: { '/path': { get: { @@ -111,12 +114,18 @@ testRule('oas2-operation-security-defined', [ apikey: [], basic: [], }, + {}, ], }, }, }, }, errors: [ + { + message: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.', + path: ['paths', '/path', 'get', 'security', '0', 'apikey'], + severity: DiagnosticSeverity.Warning, + }, { message: 'Operation "security" values must match a scheme defined in the "securityDefinitions" object.', path: ['paths', '/path', 'get', 'security', '0', 'basic'], @@ -126,28 +135,58 @@ testRule('oas2-operation-security-defined', [ }, { - name: 'valid and invalid object (API-level security)', + name: 'invalid scopes in Security Scheme object', document: { swagger: '2.0', securityDefinitions: { - apikey: {}, - }, - security: [ - { - apikey: [], - basic: [], + authorizationCode: { + type: 'oauth2', + flows: 'accessCode', + authorizationUrl: 'https://example.com/api/oauth/dialog', + tokenUrl: 'https://example.com/api/oauth/token', + scopes: null, }, - ], + noFlows: { + type: 'oauth2', + }, + client: { + type: 'oauth2', + flows: { + clientCredentials: null, + }, + }, + }, paths: { '/path': { - get: {}, + get: { + security: [ + { + noFlows: ['read:users'], + authorizationCode: ['write:users'], + }, + { + noFlows: [], + client: ['read:users'], + }, + ], + }, }, }, }, errors: [ { - message: 'API "security" values must match a scheme defined in the "securityDefinitions" object.', - path: ['security', '0', 'basic'], + message: '"read:users" must be listed among scopes.', + path: ['paths', '/path', 'get', 'security', '0', 'noFlows', '0'], + severity: DiagnosticSeverity.Warning, + }, + { + message: '"write:users" must be listed among scopes.', + path: ['paths', '/path', 'get', 'security', '0', 'authorizationCode', '0'], + severity: DiagnosticSeverity.Warning, + }, + { + message: '"read:users" must be listed among scopes.', + path: ['paths', '/path', 'get', 'security', '1', 'client', '0'], severity: DiagnosticSeverity.Warning, }, ], 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 b024adc30..2046871f7 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 @@ -3,14 +3,23 @@ import testRule from './__helpers__/tester'; testRule('oas3-operation-security-defined', [ { - name: 'validate a correct object (just in body)', + name: 'valid case', document: { openapi: '3.0.2', components: { securitySchemes: { - apikey: {}, + apikey: { + type: 'apiKey', + name: 'api_key', + in: 'header', + }, }, }, + security: [ + { + apikey: [], + }, + ], paths: { '/path': { get: { @@ -25,40 +34,51 @@ testRule('oas3-operation-security-defined', [ }, errors: [], }, + { - name: 'validate a correct object (API-level security)', + name: 'valid and invalid object', document: { openapi: '3.0.2', components: { securitySchemes: { - apikey: {}, - }, - security: [ - { - apikey: [], + apikey: { + type: 'apiKey', + name: 'api_key', + in: 'header', + }, + oauth2: { + type: 'oauth2', + flows: { + authorizationCode: { + authorizationUrl: 'https://example.com/api/oauth/dialog', + tokenUrl: 'https://example.com/api/oauth/token', + scopes: { + 'write:pets': 'modify pets in your account', + 'read:pets': 'read your pets', + }, + }, + }, }, - ], - }, - paths: { - '/path': { - get: {}, }, }, - }, - errors: [], - }, - - { - name: 'return errors on invalid object', - document: { - openapi: '3.0.2', - components: {}, + security: [ + { + apikey: [], + basic: [], + oauth2: ['write:pets'], + }, + {}, + { + oauth2: ['write:users', 'read:users'], + }, + ], paths: { - '/path': { + '/users': { get: { security: [ { - apikey: [], + bearer: [], + oauth2: [], }, ], }, @@ -67,47 +87,32 @@ testRule('oas3-operation-security-defined', [ }, errors: [ { - message: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.', - path: ['paths', '/path', 'get', 'security', '0', 'apikey'], + message: 'API "security" values must match a scheme defined in the "components.securitySchemes" object.', + path: ['security', '0', 'basic'], severity: DiagnosticSeverity.Warning, }, - ], - }, - - { - name: 'return errors on invalid object (API-level)', - document: { - openapi: '3.0.2', - components: {}, - security: [ - { - apikey: [], - }, - ], - paths: { - '/path': { - get: {}, - }, + { + message: '"write:users" must be listed among scopes.', + path: ['security', '2', 'oauth2', '0'], + severity: DiagnosticSeverity.Warning, }, - }, - errors: [ { - message: 'API "security" values must match a scheme defined in the "components.securitySchemes" object.', - path: ['security', '0', 'apikey'], + message: '"read:users" must be listed among scopes.', + path: ['security', '2', 'oauth2', '1'], + severity: DiagnosticSeverity.Warning, + }, + { + message: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.', + path: ['paths', '/users', 'get', 'security', '0', 'bearer'], severity: DiagnosticSeverity.Warning, }, ], }, { - name: 'return errors on valid and invalid object', + name: 'missing securitySchemes', document: { - openapi: '3.0.2', - components: { - securitySchemes: { - apikey: {}, - }, - }, + openapi: '3.1.0', paths: { '/path': { get: { @@ -123,6 +128,11 @@ testRule('oas3-operation-security-defined', [ }, }, errors: [ + { + message: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.', + path: ['paths', '/path', 'get', 'security', '0', 'apikey'], + severity: DiagnosticSeverity.Warning, + }, { message: 'Operation "security" values must match a scheme defined in the "components.securitySchemes" object.', path: ['paths', '/path', 'get', 'security', '0', 'basic'], @@ -132,30 +142,70 @@ testRule('oas3-operation-security-defined', [ }, { - name: 'valid and invalid object (API-level security)', + name: 'invalid scopes in Security Scheme object', document: { - openapi: '3.0.2', + openapi: '3.1.0', components: { securitySchemes: { - apikey: {}, + authorizationCode: { + type: 'oauth2', + flows: { + authorizationCode: { + authorizationUrl: 'https://example.com/api/oauth/dialog', + tokenUrl: 'https://example.com/api/oauth/token', + scopes: null, + }, + }, + }, + noFlows: { + type: 'oauth2', + }, + client: { + type: 'oauth2', + flows: { + clientCredentials: null, + }, + }, + broken: null, }, }, - security: [ - { - apikey: [], - basic: [], - }, - ], paths: { '/path': { - get: {}, + get: { + security: [ + { + noFlows: ['read:users'], + authorizationCode: ['write:users'], + broken: ['delete:users'], + }, + { + noFlows: [], + client: ['read:users'], + }, + ], + }, }, }, }, errors: [ { - message: 'API "security" values must match a scheme defined in the "components.securitySchemes" object.', - path: ['security', '0', 'basic'], + message: '"read:users" must be listed among scopes.', + path: ['paths', '/path', 'get', 'security', '0', 'noFlows', '0'], + severity: DiagnosticSeverity.Warning, + }, + { + message: '"write:users" must be listed among scopes.', + path: ['paths', '/path', 'get', 'security', '0', 'authorizationCode', '0'], + severity: DiagnosticSeverity.Warning, + }, + { + message: '"delete:users" must be listed among scopes.', + path: ['paths', '/path', 'get', 'security', '0', 'broken', '0'], + severity: DiagnosticSeverity.Warning, + }, + { + message: '"read:users" must be listed among scopes.', + path: ['paths', '/path', 'get', 'security', '1', 'client', '0'], severity: DiagnosticSeverity.Warning, }, ], diff --git a/packages/rulesets/src/oas/functions/oasSecurityDefined.ts b/packages/rulesets/src/oas/functions/oasSecurityDefined.ts index bb273fd7d..1043a9bc2 100644 --- a/packages/rulesets/src/oas/functions/oasSecurityDefined.ts +++ b/packages/rulesets/src/oas/functions/oasSecurityDefined.ts @@ -6,10 +6,16 @@ type Options = { oasVersion: 2 | 3; }; -export default createRulesetFunction, Options>( +export default createRulesetFunction, Options>( { input: { type: 'object', + additionalProperties: { + type: 'array', + items: { + type: 'string', + }, + }, }, options: { type: 'object', @@ -45,9 +51,41 @@ export default createRulesetFunction, Options>( message: `${object} "security" values must match a scheme defined in the "${location}" object.`, path: [...path, schemeName], }); + + continue; + } + + const scope = input[schemeName]; + for (let i = 0; i < scope.length; i++) { + const scopeName = scope[i]; + if (!isScopeDefined(oasVersion, scopeName, allDefs[schemeName])) { + results ??= []; + results.push({ + message: `"${scopeName}" must be listed among scopes.`, + path: [...path, schemeName, i], + }); + } } } return results; }, ); + +function isScopeDefined(oasVersion: 2 | 3, scopeName: string, securityScheme: unknown): boolean { + if (!isPlainObject(securityScheme)) return false; + + if (oasVersion === 2) { + return isPlainObject(securityScheme.scopes) && scopeName in securityScheme.scopes; + } + + if (isPlainObject(securityScheme.flows)) { + for (const flow of Object.values(securityScheme.flows)) { + if (isPlainObject(flow) && isPlainObject(flow.scopes) && scopeName in flow.scopes) { + return true; + } + } + } + + return false; +}