From 33565913a51ccce88616c5260d2a766dec96f286 Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Tue, 12 Apr 2022 19:48:04 +0200 Subject: [PATCH 1/5] feat(rulesets): add rules to validate channels.servers and server.security --- docs/reference/asyncapi-rules.md | 48 ++++ .../asyncapi-channel-servers.test.ts | 118 +++++++++ .../asyncapi-server-security.test.ts | 225 ++++++++++++++++++ .../functions/asyncApi2ChannelServers.ts | 54 +++++ .../asyncapi/functions/asyncApi2Security.ts | 75 ++++++ packages/rulesets/src/asyncapi/index.ts | 24 ++ 6 files changed, 544 insertions(+) create mode 100644 packages/rulesets/src/asyncapi/__tests__/asyncapi-channel-servers.test.ts create mode 100644 packages/rulesets/src/asyncapi/__tests__/asyncapi-server-security.test.ts create mode 100644 packages/rulesets/src/asyncapi/functions/asyncApi2ChannelServers.ts create mode 100644 packages/rulesets/src/asyncapi/functions/asyncApi2Security.ts diff --git a/docs/reference/asyncapi-rules.md b/docs/reference/asyncapi-rules.md index 92f460ae2..cf422d3a3 100644 --- a/docs/reference/asyncapi-rules.md +++ b/docs/reference/asyncapi-rules.md @@ -30,6 +30,48 @@ All channel parameters should be defined in the `parameters` object of the chann **Recommended:** Yes +### asyncapi-channel-servers + +Channel servers must be defined in the `servers` object. + +**Bad Example** + +```yaml +asyncapi: "2.0.0" +info: + title: Awesome API + description: A very well defined API + version: "1.0" +servers: + production: + url: "stoplight.io" + protocol: "https" +channels: + hello: + servers: + - development +``` + +**Good Example** + +```yaml +asyncapi: "2.0.0" +info: + title: Awesome API + description: A very well defined API + version: "1.0" +servers: + production: + url: "stoplight.io" + protocol: "https" +channels: + hello: + servers: + - production +``` + +**Recommended:** Yes + ### asyncapi-headers-schema-type-object The schema definition of the application headers must be of type “object”. @@ -369,6 +411,12 @@ Server URL should not point at example.com. **Recommended:** No +### asyncapi-server-security + +Server `security` values must match a scheme defined in the `components.securitySchemes` object. It also checks if there are `oauth2` scopes that have been defined for the given security. + +**Recommended:** Yes + ### asyncapi-server-variables All server URL variables should be defined in the `variables` object of the server. They should also not contain redundant variables that do not exist in the server address. diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-channel-servers.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-channel-servers.test.ts new file mode 100644 index 000000000..48c7c7451 --- /dev/null +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-channel-servers.test.ts @@ -0,0 +1,118 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import testRule from './__helpers__/tester'; + +testRule('asyncapi-channel-servers', [ + { + name: 'valid case', + document: { + asyncapi: '2.2.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: { + servers: ['development'], + }, + }, + }, + errors: [], + }, + + { + name: 'valid case - without defined servers', + document: { + asyncapi: '2.2.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: {}, + }, + }, + errors: [], + }, + + { + name: 'valid case - with empty array', + document: { + asyncapi: '2.2.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: { + servers: [], + }, + }, + }, + errors: [], + }, + + { + name: 'invalid case', + document: { + asyncapi: '2.2.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: { + servers: ['another-server'], + }, + }, + }, + errors: [ + { + message: 'Channel contains server that are not defined on the "servers" object.', + path: ['channels', 'channel', 'servers', '0'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case - one server is defined, another one not', + document: { + asyncapi: '2.2.0', + servers: { + development: {}, + production: {}, + }, + channels: { + channel: { + servers: ['production', 'another-server'], + }, + }, + }, + errors: [ + { + message: 'Channel contains server that are not defined on the "servers" object.', + path: ['channels', 'channel', 'servers', '1'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case - without defined servers', + document: { + asyncapi: '2.2.0', + channels: { + channel: { + servers: ['production'], + }, + }, + }, + errors: [ + { + message: 'Channel contains server that are not defined on the "servers" object.', + path: ['channels', 'channel', 'servers', '0'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-security.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-security.test.ts new file mode 100644 index 000000000..4ed7afbed --- /dev/null +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-security.test.ts @@ -0,0 +1,225 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import testRule from './__helpers__/tester'; + +testRule('asyncapi-server-security', [ + { + name: 'valid case', + document: { + asyncapi: '2.0.0', + servers: { + production: { + security: [ + { + petstore_auth: [], + }, + ], + }, + }, + components: { + securitySchemes: { + petstore_auth: {}, + }, + }, + }, + errors: [], + }, + + { + name: 'valid case (oauth2)', + document: { + asyncapi: '2.0.0', + servers: { + production: { + security: [ + { + petstore_auth: ['write:pets'], + }, + ], + }, + }, + components: { + securitySchemes: { + petstore_auth: { + type: 'oauth2', + flows: { + implicit: { + scopes: { + 'write:pets': '...', + 'read:pets': '...', + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, + + { + name: 'invalid case', + document: { + asyncapi: '2.0.0', + servers: { + production: { + security: [ + { + not_defined: [], + }, + ], + }, + }, + components: { + securitySchemes: { + petstore_auth: {}, + }, + }, + }, + errors: [ + { + message: 'Server must not reference an undefined security scheme.', + path: ['servers', 'production', 'security', '0', 'not_defined'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case (oauth2)', + document: { + asyncapi: '2.0.0', + servers: { + production: { + security: [ + { + petstore_auth: ['write:pets', 'not:defined'], + }, + ], + }, + }, + components: { + securitySchemes: { + petstore_auth: { + type: 'oauth2', + flows: { + implicit: { + scopes: { + 'write:pets': '...', + 'read:pets': '...', + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + message: 'Non-existing security scope for the specified security scheme. Available: [write:pets, read:pets]', + path: ['servers', 'production', 'security', '0', 'petstore_auth', '1'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case (oauth2) - multiple flows and not defined scopes', + document: { + asyncapi: '2.0.0', + servers: { + production: { + security: [ + { + petstore_auth: ['write:pets', 'not:defined1', 'not:defined2'], + }, + ], + }, + }, + components: { + securitySchemes: { + petstore_auth: { + type: 'oauth2', + flows: { + implicit: { + scopes: { + 'write:pets': '...', + 'read:pets': '...', + }, + }, + password: { + scopes: { + 'write:dogs': '...', + 'read:dogs': '...', + }, + }, + clientCredentials: { + scopes: { + 'write:cats': '...', + 'read:cats': '...', + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + message: + 'Non-existing security scope for the specified security scheme. Available: [write:pets, read:pets, write:dogs, read:dogs, write:cats, read:cats]', + path: ['servers', 'production', 'security', '0', 'petstore_auth', '1'], + severity: DiagnosticSeverity.Error, + }, + { + message: + 'Non-existing security scope for the specified security scheme. Available: [write:pets, read:pets, write:dogs, read:dogs, write:cats, read:cats]', + path: ['servers', 'production', 'security', '0', 'petstore_auth', '2'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case (oauth2) - not valid flow', + document: { + asyncapi: '2.0.0', + servers: { + production: { + security: [ + { + petstore_auth: ['write:pets', 'not:defined'], + }, + ], + }, + }, + components: { + securitySchemes: { + petstore_auth: { + type: 'oauth2', + flows: { + implicit: { + scopes: { + 'write:pets': '...', + 'read:pets': '...', + }, + }, + notDefinedFlow: { + scopes: { + 'write:dogs': '...', + 'read:dogs': '...', + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + message: 'Non-existing security scope for the specified security scheme. Available: [write:pets, read:pets]', + path: ['servers', 'production', 'security', '0', 'petstore_auth', '1'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2ChannelServers.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2ChannelServers.ts new file mode 100644 index 000000000..af89075a2 --- /dev/null +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2ChannelServers.ts @@ -0,0 +1,54 @@ +import { createRulesetFunction } from '@stoplight/spectral-core'; + +import type { IFunctionResult } from '@stoplight/spectral-core'; + +export default createRulesetFunction< + { servers?: Record; channels?: Record }> }, + null +>( + { + input: { + type: 'object', + properties: { + servers: { + type: 'object', + }, + channels: { + type: 'object', + additionalProperties: { + type: 'object', + properties: { + servers: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + options: null, + }, + function asyncApi2ChannelServers(targetVal, _) { + const results: IFunctionResult[] = []; + if (!targetVal.channels) return results; + const serverNames = Object.keys(targetVal.servers ?? {}); + + Object.entries(targetVal.channels).forEach(([channelAddress, channel]) => { + if (!channel.servers) return; + + channel.servers.forEach((serverName, index) => { + if (!serverNames.includes(serverName)) { + results.push({ + message: `Channel contains server that are not defined on the "servers" object.`, + path: ['channels', channelAddress, 'servers', index], + }); + } + }); + }); + + return results; + }, +); diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2Security.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2Security.ts new file mode 100644 index 000000000..dffb0281e --- /dev/null +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2Security.ts @@ -0,0 +1,75 @@ +import { createRulesetFunction } from '@stoplight/spectral-core'; + +import type { IFunctionResult } from '@stoplight/spectral-core'; +import { isPlainObject } from '@stoplight/json'; + +type Scopes = { + scopes: Record; +}; +type OAuth2Security = { + implicit?: Scopes; + password?: Scopes; + clientCredentials?: Scopes; + authorizationCode?: Scopes; +}; + +const OAuth2Keys = ['implicit', 'password', 'clientCredentials', 'authorizationCode']; +function getAllScopes(oauth2: OAuth2Security): string[] { + const scopes: string[] = []; + OAuth2Keys.forEach(key => { + const flow = oauth2[key] as Scopes; + if (isPlainObject(flow) && isPlainObject(flow)) { + scopes.push(...Object.keys(flow.scopes)); + } + }); + return Array.from(new Set(scopes)); +} + +export default createRulesetFunction, null>( + { + input: { + type: 'object', + additionalProperties: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + options: null, + }, + function asyncApi2Security(targetVal = {}, _, ctx) { + const results: IFunctionResult[] = []; + const spec = ctx.document.data as { + components: { securitySchemes: Record }; + }; + const securitySchemes = spec?.components?.securitySchemes ?? {}; + const securitySchemesKeys = Object.keys(securitySchemes); + + Object.keys(targetVal).forEach(securityKey => { + if (!securitySchemesKeys.includes(securityKey)) { + results.push({ + message: `Server must not reference an undefined security scheme.`, + path: [...ctx.path, securityKey], + }); + } + + const securityScheme = securitySchemes[securityKey]; + if (securityScheme?.type === 'oauth2') { + const availableScopes = getAllScopes(securityScheme.flows ?? {}); + targetVal[securityKey].forEach((securityScope, index) => { + if (!availableScopes.includes(securityScope)) { + results.push({ + message: `Non-existing security scope for the specified security scheme. Available: [${availableScopes.join( + ', ', + )}]`, + path: [...ctx.path, securityKey, index], + }); + } + }); + } + }); + + return results; + }, +); diff --git a/packages/rulesets/src/asyncapi/index.ts b/packages/rulesets/src/asyncapi/index.ts index f0bbe118c..a59c5d4c7 100644 --- a/packages/rulesets/src/asyncapi/index.ts +++ b/packages/rulesets/src/asyncapi/index.ts @@ -9,6 +9,7 @@ import { } from '@stoplight/spectral-functions'; import asyncApi2ChannelParameters from './functions/asyncApi2ChannelParameters'; +import asyncApi2ChannelServers from './functions/asyncApi2ChannelServers'; import asyncApi2DocumentSchema from './functions/asyncApi2DocumentSchema'; import asyncApi2MessageExamplesValidation from './functions/asyncApi2MessageExamplesValidation'; import asyncApi2OperationIdUniqueness from './functions/asyncApi2OperationIdUniqueness'; @@ -16,6 +17,7 @@ import asyncApi2SchemaValidation from './functions/asyncApi2SchemaValidation'; import asyncApi2PayloadValidation from './functions/asyncApi2PayloadValidation'; import asyncApi2ServerVariables from './functions/asyncApi2ServerVariables'; import { uniquenessTags } from '../shared/functions'; +import asyncApi2Security from './functions/asyncApi2Security'; export default { documentationUrl: 'https://meta.stoplight.io/docs/spectral/docs/reference/asyncapi-rules.md', @@ -71,6 +73,17 @@ export default { function: asyncApi2ChannelParameters, }, }, + 'asyncapi-channel-servers': { + description: 'Channel servers must be defined in the "servers" object.', + message: '{{error}}', + severity: 'error', + type: 'validation', + recommended: true, + given: '$', + then: { + function: asyncApi2ChannelServers, + }, + }, 'asyncapi-headers-schema-type-object': { description: 'Headers schema type must be "object".', message: 'Headers schema type must be "object" ({{error}}).', @@ -380,6 +393,17 @@ export default { }, }, }, + 'asyncapi-server-security': { + description: 'Server have to reference a defined security schemes.', + message: '{{error}}', + severity: 'error', + type: 'validation', + recommended: true, + given: '$.servers.*.security.*', + then: { + function: asyncApi2Security, + }, + }, 'asyncapi-servers': { description: 'AsyncAPI object must have non-empty "servers" object.', recommended: true, From dab3521cf6d84128cce9940e41f27cc298b85a2e Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Thu, 19 May 2022 13:38:15 +0200 Subject: [PATCH 2/5] feat(rulesets): check security on operation level --- .../asyncapi-channel-servers.test.ts | 23 ++ .../asyncapi-operation-security.test.ts | 321 ++++++++++++++++++ .../asyncapi-server-security.test.ts | 52 +++ .../functions/asyncApi2ChannelServers.ts | 2 +- .../asyncapi/functions/asyncApi2Security.ts | 16 +- packages/rulesets/src/asyncapi/index.ts | 17 + 6 files changed, 426 insertions(+), 5 deletions(-) create mode 100644 packages/rulesets/src/asyncapi/__tests__/asyncapi-operation-security.test.ts diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-channel-servers.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-channel-servers.test.ts index 48c7c7451..64ca5f57a 100644 --- a/packages/rulesets/src/asyncapi/__tests__/asyncapi-channel-servers.test.ts +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-channel-servers.test.ts @@ -34,6 +34,29 @@ testRule('asyncapi-channel-servers', [ errors: [], }, + { + name: 'valid case - without defined servers in the root', + document: { + asyncapi: '2.2.0', + channels: { + channel: {}, + }, + }, + errors: [], + }, + + { + name: 'valid case - without defined channels in the root', + document: { + asyncapi: '2.2.0', + servers: { + development: {}, + production: {}, + }, + }, + errors: [], + }, + { name: 'valid case - with empty array', document: { diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-operation-security.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-operation-security.test.ts new file mode 100644 index 000000000..bfc6b756e --- /dev/null +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-operation-security.test.ts @@ -0,0 +1,321 @@ +import { DiagnosticSeverity } from '@stoplight/types'; +import testRule from './__helpers__/tester'; + +testRule('asyncapi-operation-security', [ + { + name: 'valid case', + document: { + asyncapi: '2.4.0', + channels: { + channel: { + publish: { + security: [ + { + petstore_auth: [], + }, + ], + }, + }, + }, + components: { + securitySchemes: { + petstore_auth: {}, + }, + }, + }, + errors: [], + }, + + { + name: 'valid case (oauth2)', + document: { + asyncapi: '2.4.0', + channels: { + channel: { + publish: { + security: [ + { + petstore_auth: ['write:pets'], + }, + ], + }, + }, + }, + components: { + securitySchemes: { + petstore_auth: { + type: 'oauth2', + flows: { + implicit: { + scopes: { + 'write:pets': '...', + 'read:pets': '...', + }, + }, + }, + }, + }, + }, + }, + errors: [], + }, + + { + name: 'invalid case (publish operation)', + document: { + asyncapi: '2.4.0', + channels: { + channel: { + publish: { + security: [ + { + not_defined: [], + }, + ], + }, + }, + }, + components: { + securitySchemes: { + petstore_auth: {}, + }, + }, + }, + errors: [ + { + message: 'Operation must not reference an undefined security scheme.', + path: ['channels', 'channel', 'publish', 'security', '0', 'not_defined'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case (subscribe operation)', + document: { + asyncapi: '2.4.0', + channels: { + channel: { + subscribe: { + security: [ + { + not_defined: [], + }, + ], + }, + }, + }, + components: { + securitySchemes: { + petstore_auth: {}, + }, + }, + }, + errors: [ + { + message: 'Operation must not reference an undefined security scheme.', + path: ['channels', 'channel', 'subscribe', 'security', '0', 'not_defined'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case (oauth2)', + document: { + asyncapi: '2.4.0', + channels: { + channel: { + publish: { + security: [ + { + petstore_auth: ['write:pets', 'not:defined'], + }, + ], + }, + }, + }, + components: { + securitySchemes: { + petstore_auth: { + type: 'oauth2', + flows: { + implicit: { + scopes: { + 'write:pets': '...', + 'read:pets': '...', + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + message: 'Non-existing security scope for the specified security scheme. Available: [write:pets, read:pets]', + path: ['channels', 'channel', 'publish', 'security', '0', 'petstore_auth', '1'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case (oauth2) - multiple flows and not defined scopes', + document: { + asyncapi: '2.4.0', + channels: { + channel: { + publish: { + security: [ + { + petstore_auth: ['write:pets', 'not:defined1', 'not:defined2'], + }, + ], + }, + }, + }, + components: { + securitySchemes: { + petstore_auth: { + type: 'oauth2', + flows: { + implicit: { + scopes: { + 'write:pets': '...', + 'read:pets': '...', + }, + }, + password: { + scopes: { + 'write:dogs': '...', + 'read:dogs': '...', + }, + }, + clientCredentials: { + scopes: { + 'write:cats': '...', + 'read:cats': '...', + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + message: + 'Non-existing security scope for the specified security scheme. Available: [write:pets, read:pets, write:dogs, read:dogs, write:cats, read:cats]', + path: ['channels', 'channel', 'publish', 'security', '0', 'petstore_auth', '1'], + severity: DiagnosticSeverity.Error, + }, + { + message: + 'Non-existing security scope for the specified security scheme. Available: [write:pets, read:pets, write:dogs, read:dogs, write:cats, read:cats]', + path: ['channels', 'channel', 'publish', 'security', '0', 'petstore_auth', '2'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case (oauth2) - not valid flow', + document: { + asyncapi: '2.4.0', + channels: { + channel: { + publish: { + security: [ + { + petstore_auth: ['write:pets', 'not:defined'], + }, + ], + }, + }, + }, + components: { + securitySchemes: { + petstore_auth: { + type: 'oauth2', + flows: { + implicit: { + scopes: { + 'write:pets': '...', + 'read:pets': '...', + }, + }, + notDefinedFlow: { + scopes: { + 'write:dogs': '...', + 'read:dogs': '...', + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + message: 'Non-existing security scope for the specified security scheme. Available: [write:pets, read:pets]', + path: ['channels', 'channel', 'publish', 'security', '0', 'petstore_auth', '1'], + severity: DiagnosticSeverity.Error, + }, + ], + }, + + { + name: 'invalid case (multiple securities)', + document: { + asyncapi: '2.4.0', + channels: { + channel: { + publish: { + security: [ + { + not_defined: [], + }, + { + petstore_auth: ['write:pets', 'not:defined'], + }, + ], + }, + }, + }, + components: { + securitySchemes: { + petstore_auth: { + type: 'oauth2', + flows: { + implicit: { + scopes: { + 'write:pets': '...', + 'read:pets': '...', + }, + }, + notDefinedFlow: { + scopes: { + 'write:dogs': '...', + 'read:dogs': '...', + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + message: 'Operation must not reference an undefined security scheme.', + path: ['channels', 'channel', 'publish', 'security', '0', 'not_defined'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Non-existing security scope for the specified security scheme. Available: [write:pets, read:pets]', + path: ['channels', 'channel', 'publish', 'security', '1', 'petstore_auth', '1'], + severity: DiagnosticSeverity.Error, + }, + ], + }, +]); diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-security.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-security.test.ts index 4ed7afbed..44b1840a8 100644 --- a/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-security.test.ts +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-security.test.ts @@ -222,4 +222,56 @@ testRule('asyncapi-server-security', [ }, ], }, + + { + name: 'invalid case (multiple securities)', + document: { + asyncapi: '2.0.0', + servers: { + production: { + security: [ + { + not_defined: [], + }, + { + petstore_auth: ['write:pets', 'not:defined'], + }, + ], + }, + }, + components: { + securitySchemes: { + petstore_auth: { + type: 'oauth2', + flows: { + implicit: { + scopes: { + 'write:pets': '...', + 'read:pets': '...', + }, + }, + notDefinedFlow: { + scopes: { + 'write:dogs': '...', + 'read:dogs': '...', + }, + }, + }, + }, + }, + }, + }, + errors: [ + { + message: 'Server must not reference an undefined security scheme.', + path: ['servers', 'production', 'security', '0', 'not_defined'], + severity: DiagnosticSeverity.Error, + }, + { + message: 'Non-existing security scope for the specified security scheme. Available: [write:pets, read:pets]', + path: ['servers', 'production', 'security', '1', 'petstore_auth', '1'], + severity: DiagnosticSeverity.Error, + }, + ], + }, ]); diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2ChannelServers.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2ChannelServers.ts index af89075a2..573c13b35 100644 --- a/packages/rulesets/src/asyncapi/functions/asyncApi2ChannelServers.ts +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2ChannelServers.ts @@ -36,7 +36,7 @@ export default createRulesetFunction< if (!targetVal.channels) return results; const serverNames = Object.keys(targetVal.servers ?? {}); - Object.entries(targetVal.channels).forEach(([channelAddress, channel]) => { + Object.entries(targetVal.channels ?? {}).forEach(([channelAddress, channel]) => { if (!channel.servers) return; channel.servers.forEach((serverName, index) => { diff --git a/packages/rulesets/src/asyncapi/functions/asyncApi2Security.ts b/packages/rulesets/src/asyncapi/functions/asyncApi2Security.ts index dffb0281e..e24c938ab 100644 --- a/packages/rulesets/src/asyncapi/functions/asyncApi2Security.ts +++ b/packages/rulesets/src/asyncapi/functions/asyncApi2Security.ts @@ -25,7 +25,7 @@ function getAllScopes(oauth2: OAuth2Security): string[] { return Array.from(new Set(scopes)); } -export default createRulesetFunction, null>( +export default createRulesetFunction, { objectType: 'Server' | 'Operation' }>( { input: { type: 'object', @@ -36,9 +36,17 @@ export default createRulesetFunction, null>( }, }, }, - options: null, + options: { + type: 'object', + properties: { + objectType: { + type: 'string', + enum: ['Server', 'Operation'], + }, + }, + }, }, - function asyncApi2Security(targetVal = {}, _, ctx) { + function asyncApi2Security(targetVal = {}, { objectType }, ctx) { const results: IFunctionResult[] = []; const spec = ctx.document.data as { components: { securitySchemes: Record }; @@ -49,7 +57,7 @@ export default createRulesetFunction, null>( Object.keys(targetVal).forEach(securityKey => { if (!securitySchemesKeys.includes(securityKey)) { results.push({ - message: `Server must not reference an undefined security scheme.`, + message: `${objectType} must not reference an undefined security scheme.`, path: [...ctx.path, securityKey], }); } diff --git a/packages/rulesets/src/asyncapi/index.ts b/packages/rulesets/src/asyncapi/index.ts index a59c5d4c7..1596cf823 100644 --- a/packages/rulesets/src/asyncapi/index.ts +++ b/packages/rulesets/src/asyncapi/index.ts @@ -227,6 +227,20 @@ export default { function: truthy, }, }, + 'asyncapi-operation-security': { + description: 'Operation have to reference a defined security schemes.', + message: '{{error}}', + severity: 'error', + type: 'validation', + recommended: true, + given: '$.channels[*][publish,subscribe].security.*', + then: { + function: asyncApi2Security, + functionOptions: { + objectType: 'Operation', + }, + }, + }, 'asyncapi-parameter-description': { description: 'Parameter objects must have "description".', recommended: false, @@ -402,6 +416,9 @@ export default { given: '$.servers.*.security.*', then: { function: asyncApi2Security, + functionOptions: { + objectType: 'Server', + }, }, }, 'asyncapi-servers': { From e336c848429c0fc3bd37679004bccd6be2e44420 Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Thu, 19 May 2022 13:39:38 +0200 Subject: [PATCH 3/5] feat(rulesets): check security on operation level --- .../asyncapi-operation-security.test.ts | 19 +++++++++++++++++++ .../asyncapi-server-security.test.ts | 16 ++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-operation-security.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-operation-security.test.ts index bfc6b756e..64cb9ff65 100644 --- a/packages/rulesets/src/asyncapi/__tests__/asyncapi-operation-security.test.ts +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-operation-security.test.ts @@ -26,6 +26,25 @@ testRule('asyncapi-operation-security', [ errors: [], }, + { + name: 'valid case (without security field)', + document: { + asyncapi: '2.4.0', + channels: { + channel: { + publish: {}, + subscribe: {}, + }, + }, + components: { + securitySchemes: { + petstore_auth: {}, + }, + }, + }, + errors: [], + }, + { name: 'valid case (oauth2)', document: { diff --git a/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-security.test.ts b/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-security.test.ts index 44b1840a8..74e160a9c 100644 --- a/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-security.test.ts +++ b/packages/rulesets/src/asyncapi/__tests__/asyncapi-server-security.test.ts @@ -24,6 +24,22 @@ testRule('asyncapi-server-security', [ errors: [], }, + { + name: 'valid case (without security field)', + document: { + asyncapi: '2.0.0', + servers: { + production: {}, + }, + components: { + securitySchemes: { + petstore_auth: {}, + }, + }, + }, + errors: [], + }, + { name: 'valid case (oauth2)', document: { From 25f11767a536c0ae92384dfc9c502a90059458f5 Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Thu, 19 May 2022 13:56:42 +0200 Subject: [PATCH 4/5] feat(rulesets): update docs --- docs/reference/asyncapi-rules.md | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/docs/reference/asyncapi-rules.md b/docs/reference/asyncapi-rules.md index cf422d3a3..559839bdd 100644 --- a/docs/reference/asyncapi-rules.md +++ b/docs/reference/asyncapi-rules.md @@ -261,6 +261,38 @@ This operation ID is essentially a reference for the operation. Tools may use it **Recommended:** Yes +### asyncapi-operation-security + +Operation `security` values must match a scheme defined in the `components.securitySchemes` object. It also checks if there are `oauth2` scopes that have been defined for the given security. + +**Recommended:** Yes + +**Good Example** + +```yaml +channels: + 'user/signup': + publish: + security: + - petstore_auth: [] +components: + securitySchemes: + petstore_auth: ... +``` + +**Bad Example** + +```yaml +channels: + 'user/signup': + publish: + security: + - not_defined: [] +components: + securitySchemes: + petstore_auth: ... +``` + ### asyncapi-parameter-description Parameter objects should have a `description`. @@ -417,6 +449,32 @@ Server `security` values must match a scheme defined in the `components.security **Recommended:** Yes +**Good Example** + +```yaml +servers: + production: + url: test.mosquitto.org + security: + - petstore_auth: [] +components: + securitySchemes: + petstore_auth: ... +``` + +**Bad Example** + +```yaml +servers: + production: + url: test.mosquitto.org + security: + - not_defined: [] +components: + securitySchemes: + petstore_auth: ... +``` + ### asyncapi-server-variables All server URL variables should be defined in the `variables` object of the server. They should also not contain redundant variables that do not exist in the server address. From 906b98758dd58634e4b56b8d54d5c8aa44b005b3 Mon Sep 17 00:00:00 2001 From: Matatjahu Date: Thu, 19 May 2022 14:11:22 +0200 Subject: [PATCH 5/5] feat(rulesets): update docs --- docs/reference/asyncapi-rules.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/asyncapi-rules.md b/docs/reference/asyncapi-rules.md index 559839bdd..d22874787 100644 --- a/docs/reference/asyncapi-rules.md +++ b/docs/reference/asyncapi-rules.md @@ -271,7 +271,7 @@ Operation `security` values must match a scheme defined in the `components.secur ```yaml channels: - 'user/signup': + "user/signup": publish: security: - petstore_auth: [] @@ -284,7 +284,7 @@ components: ```yaml channels: - 'user/signup': + "user/signup": publish: security: - not_defined: []