Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* `ExampleValueFieldsBefore32`: detect Example Object `dataValue` / `serializedValue` usage in pre-3.2 documents (3.2 additions)
* `XmlNodeTypeBefore32`: detect XML Object `nodeType` usage in pre-3.2 documents (3.2 addition)
* `XmlAttributeDeprecation` / `XmlWrappedDeprecation`: detect XML Object `attribute: true` / `wrapped: true` in 3.2 documents (deprecated in 3.2 in favor of `nodeType`)
* `SecuritySchemeFieldsBefore32`: detect Security Scheme `deprecated` / `oauth2MetadataUrl` and the `deviceAuthorization` OAuth flow in pre-3.2 documents (3.2 additions)
* expose the declared version as `OpenAPI#openapi_version` (a `Gem::Version`, or nil when the field is missing or malformed) so `SpecValidator` rules compare version ranges; a 3.2 document is checked by the 3.1-or-later rules
* support 3.1-style numeric `exclusiveMinimum` / `exclusiveMaximum` in value validation (standalone bound, not a Boolean modifier on `minimum` / `maximum`)
* support `type: "null"` (3.1 primitive) in value validation
Expand Down
2 changes: 2 additions & 0 deletions lib/openapi_parser/spec_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
require_relative 'spec_validator/rules/xml_node_type_before_32'
require_relative 'spec_validator/rules/xml_attribute_deprecation'
require_relative 'spec_validator/rules/xml_wrapped_deprecation'
require_relative 'spec_validator/rules/security_scheme_fields_before_32'

module OpenAPIParser
class SpecViolationError < OpenAPIError
Expand Down Expand Up @@ -93,6 +94,7 @@ def rules
Rules::XmlNodeTypeBefore32,
Rules::XmlAttributeDeprecation,
Rules::XmlWrappedDeprecation,
Rules::SecuritySchemeFieldsBefore32,
]
end
end
Expand Down
12 changes: 12 additions & 0 deletions lib/openapi_parser/spec_validator/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ def each_node(root, *klasses)
end
end

# security schemes are not modeled by the parse layer, so rules
# about them read the raw `components.securitySchemes` map
def each_security_scheme(root)
components = root.raw_schema.is_a?(Hash) ? root.raw_schema['components'] : nil
schemes = components.is_a?(Hash) ? components['securitySchemes'] : nil
return unless schemes.is_a?(Hash)

schemes.each do |name, scheme|
yield(name, scheme) if scheme.is_a?(Hash)
end
end

# escapes a map key for use in a violation path, matching object_reference
def escape_reference(key)
key.to_s.gsub('/', '~1')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
module OpenAPIParser
class SpecValidator
module Rules
# 3.2 adds `deprecated` and `oauth2MetadataUrl` to the Security Scheme
# Object and the `deviceAuthorization` OAuth flow. Security schemes are
# not modeled by the parse layer, so this rule inspects the raw
# `components.securitySchemes` map; schemes given as a `$ref` are not
# followed.
class SecuritySchemeFieldsBefore32 < Rule
NEW_FIELDS = %w[deprecated oauth2MetadataUrl].freeze

def check(root)
return [] unless version_before?('3.2')

violations = []
each_security_scheme(root) do |name, scheme|
base = "#/components/securitySchemes/#{escape_reference(name)}"

NEW_FIELDS.each do |field|
next unless scheme.key?(field)

violations << violation(
path: "#{base}/#{field}",
message: "`#{field}` on a Security Scheme Object is a 3.2 addition; earlier documents have no such field",
)
end

flows = scheme['flows']
next unless flows.is_a?(Hash) && flows.key?('deviceAuthorization')

violations << violation(
path: "#{base}/flows/deviceAuthorization",
message: 'the `deviceAuthorization` OAuth flow is a 3.2 addition; earlier documents have no such flow',
)
end
violations
end
end
end
end
end
6 changes: 6 additions & 0 deletions sig/openapi_parser/spec_validator.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module OpenAPIParser
private def violation: (path: String, message: String) -> SpecViolation
private def each_schema: (untyped root) ?{ (untyped) -> void } -> untyped
private def each_node: (untyped root, *Class klasses) { (untyped) -> void } -> void
private def each_security_scheme: (untyped root) { (String, Hash[untyped, untyped]) -> void } -> void
private def escape_reference: (untyped key) -> String
private def walk: (untyped node, Hash[Integer, bool] visited) { (untyped) -> void } -> void
end
Expand Down Expand Up @@ -134,6 +135,11 @@ module OpenAPIParser
class XmlWrappedDeprecation < Rule
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
end

class SecuritySchemeFieldsBefore32 < Rule
NEW_FIELDS: Array[String]
def check: (OpenAPIParser::Schemas::OpenAPI root) -> Array[SpecValidator::SpecViolation]
end
end
end

Expand Down
25 changes: 25 additions & 0 deletions spec/data/openapi_3_2/security_scheme_fields_31.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
openapi: 3.1.0
info:
title: Pet API
version: '1.0'
paths:
/pets:
get:
summary: List pets
responses:
'200':
description: OK
components:
securitySchemes:
OAuth2:
type: oauth2
# `deprecated`, `oauth2MetadataUrl`, and the `deviceAuthorization`
# flow are 3.2 additions to the Security Scheme Object; a 3.1
# document has no such fields, so their use here is a spec violation.
deprecated: true
oauth2MetadataUrl: https://auth.example.com/.well-known/oauth-authorization-server
flows:
deviceAuthorization:
deviceAuthorizationUrl: https://auth.example.com/device
tokenUrl: https://auth.example.com/token
scopes: {}
25 changes: 25 additions & 0 deletions spec/data/openapi_3_2/security_scheme_fields_32.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
openapi: 3.2.0
info:
title: Pet API
version: '1.0'
paths:
/pets:
get:
summary: List pets
responses:
'200':
description: OK
components:
securitySchemes:
OAuth2:
type: oauth2
# `deprecated`, `oauth2MetadataUrl`, and the `deviceAuthorization`
# flow are legitimate Security Scheme fields under 3.2, so no
# violation is expected here.
deprecated: true
oauth2MetadataUrl: https://auth.example.com/.well-known/oauth-authorization-server
flows:
deviceAuthorization:
deviceAuthorizationUrl: https://auth.example.com/device
tokenUrl: https://auth.example.com/token
scopes: {}
14 changes: 14 additions & 0 deletions spec/openapi_parser/spec_validator/integration_3_2_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,18 @@ def expect_clean(file)
expect_clean('xml_deprecated_fields_31.yaml')
end
end

describe 'Security Scheme deviceAuthorization/oauth2MetadataUrl/deprecated (3.2 additions)' do
it 'warns on the version-mismatched document under :warn' do
expect_mismatch_warns('security_scheme_fields_31.yaml', [:security_scheme_fields_before32] * 3)
end

it 'raises SpecViolationError on the version-mismatched document under :raise' do
expect_mismatch_raises('security_scheme_fields_31.yaml', [:security_scheme_fields_before32] * 3)
end

it 'stays clean on the correctly-versioned document' do
expect_clean('security_scheme_fields_32.yaml')
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
require_relative '../../../spec_helper'

RSpec.describe 'OpenAPIParser::SpecValidator::Rules::SecuritySchemeFieldsBefore32' do
def base_doc(openapi_version_string, schemes)
{
'openapi' => openapi_version_string,
'info' => { 'title' => 'test', 'version' => '1.0' },
'paths' => {},
'components' => { 'securitySchemes' => schemes },
}
end

def doc_with_new_fields(openapi_version_string)
scheme = {
'type' => 'oauth2',
'deprecated' => true,
'oauth2MetadataUrl' => 'https://auth.example.com/.well-known/oauth-authorization-server',
'flows' => {
'deviceAuthorization' => {
'deviceAuthorizationUrl' => 'https://auth.example.com/device',
'tokenUrl' => 'https://auth.example.com/token',
'scopes' => {},
},
},
}
OpenAPIParser.parse(base_doc(openapi_version_string, { 'OAuth2' => scheme }), strict_reference_validation: false)
end

def doc_without_new_fields(openapi_version_string)
scheme = {
'type' => 'oauth2',
'flows' => { 'clientCredentials' => { 'tokenUrl' => 'https://auth.example.com/token', 'scopes' => {} } },
}
OpenAPIParser.parse(base_doc(openapi_version_string, { 'OAuth2' => scheme }), strict_reference_validation: false)
end

def run_rule_for(root)
OpenAPIParser::SpecValidator::Rules::SecuritySchemeFieldsBefore32.new(root.openapi_version).check(root)
end

context 'with a 3.2 document using the new fields' do
it 'reports no violation' do
root = doc_with_new_fields('3.2.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.2 document without the new fields' do
it 'reports no violation' do
root = doc_without_new_fields('3.2.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.1 document using the new fields' do
it 'reports one violation per offending field' do
root = doc_with_new_fields('3.1.0')
violations = run_rule_for(root)
expect(violations.map(&:path)).to match_array [
'#/components/securitySchemes/OAuth2/deprecated',
'#/components/securitySchemes/OAuth2/oauth2MetadataUrl',
'#/components/securitySchemes/OAuth2/flows/deviceAuthorization',
]
expect(violations.first.rule_name).to eq :security_scheme_fields_before32
end
end

context 'with a 3.1 document without the new fields' do
it 'reports no violation' do
root = doc_without_new_fields('3.1.0')
expect(run_rule_for(root)).to eq []
end
end

context 'with a 3.0 document using the new fields' do
it 'reports violations' do
root = doc_with_new_fields('3.0.0')
expect(run_rule_for(root).size).to eq 3
end
end

context 'with a 3.1 document whose scheme name contains a slash' do
it 'escapes the name in the violation path' do
raw = base_doc('3.1.0', { 'auth/v2' => { 'type' => 'http', 'scheme' => 'bearer', 'deprecated' => true } })
root = OpenAPIParser.parse(raw, strict_reference_validation: false)
expect(run_rule_for(root).map(&:path)).to eq ['#/components/securitySchemes/auth~1v2/deprecated']
end
end

context 'with a document whose openapi field is not a version' do
it 'reports no violation (rule skipped)' do
root = doc_with_new_fields('not-a-version')
expect(run_rule_for(root)).to eq []
end
end
end