Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove parameters that conflict with security schemas #166

3 changes: 2 additions & 1 deletion lib/rspec/openapi/result_recorder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def record_results!
config_file = File.join(File.dirname(path), RSpec::OpenAPI.config_filename)
begin
require config_file if File.exist?(config_file)
rescue => e
rescue StandardError => e
puts "WARNING: Unable to load #{config_file}: #{e}"
end

Expand All @@ -29,6 +29,7 @@ def record_results!
rescue StandardError, NotImplementedError => e # e.g. SchemaBuilder raises a NotImplementedError
@error_records[e] = record # Avoid failing the build
end
RSpec::OpenAPI::SchemaCleaner.cleanup_conflicting_security_parameters!(spec)
RSpec::OpenAPI::SchemaCleaner.cleanup!(spec, new_from_zero)
RSpec::OpenAPI::ComponentsUpdater.update!(spec, new_from_zero)
RSpec::OpenAPI::SchemaCleaner.cleanup_empty_required_array!(spec)
Expand Down
30 changes: 30 additions & 0 deletions lib/rspec/openapi/schema_cleaner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ def cleanup!(base, spec)
base
end

def cleanup_conflicting_security_parameters!(base)
Fixed Show fixed Hide fixed
security_schemes = base.dig('components', 'securitySchemes') || {}

return if security_schemes.empty?

paths_to_security_definitions = RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'paths', 'security')

paths_to_security_definitions.each do |path|
parent_path_definition = base.dig(*path.take(path.length - 1))

security_schemes.each do |security_scheme_name, security_scheme|
remove_parameters_conflicting_with_security_sceheme!(
parent_path_definition, security_scheme, security_scheme_name,
)
end
end
end

def cleanup_empty_required_array!(base)
paths_to_objects = [
*RSpec::OpenAPI::HashHelper.matched_paths_deeply_nested(base, 'components.schemas', 'properties'),
Expand All @@ -53,6 +71,18 @@ def cleanup_empty_required_array!(base)

private

def remove_parameters_conflicting_with_security_sceheme!(path_definition, security_scheme, security_scheme_name)
return unless path_definition['security']
return unless path_definition['parameters']
return unless path_definition.dig('security', 0).keys.include?(security_scheme_name)

path_definition['parameters'].reject! do |parameter|
parameter['in'] == security_scheme['in'] && # same location (ie. header)
parameter['name'] == security_scheme['name'] # same name (ie. AUTHORIZATION)
end
path_definition.delete('parameters') if path_definition['parameters'].empty?
end

def cleanup_array!(base, spec, selector, fields_for_identity = [])
marshal = lambda do |obj|
Marshal.dump(slice(obj, fields_for_identity))
Expand Down
20 changes: 20 additions & 0 deletions spec/integration_tests/rails_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
url: 'https://www.apache.org/licenses/LICENSE-2.0.html',
},
}
RSpec::OpenAPI.security_schemes = {
SecretApiKeyAuth: {
type: 'apiKey',
in: 'header',
name: 'Secret-Key',
},
}

class TablesIndexTest < ActionDispatch::IntegrationTest
i_suck_and_my_tests_are_order_dependent!
Expand Down Expand Up @@ -196,6 +203,19 @@ class ImageTest < ActionDispatch::IntegrationTest
end
end

class SecretKeyTest < ActionDispatch::IntegrationTest
i_suck_and_my_tests_are_order_dependent!
openapi!

test 'authorizes with secret key' do
get '/secret_items',
headers: {
'Secret-Key' => '42',
}
assert_response 200
end
end

class ExtraRoutesTest < ActionDispatch::IntegrationTest
i_suck_and_my_tests_are_order_dependent!
openapi!
Expand Down
5 changes: 5 additions & 0 deletions spec/rails/app/controllers/secret_items_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class SecretItemsController < ApplicationController
def index
render json: { items: ['secrets'] }
end
end
2 changes: 2 additions & 0 deletions spec/rails/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@
end

get '/test_block' => ->(_env) { [200, { 'Content-Type' => 'text/plain' }, ['A TEST']] }

get '/secret_items' => 'secret_items#index'
end
end
63 changes: 63 additions & 0 deletions spec/rails/doc/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,60 @@
}
}
},
"/secret_items": {
"get": {
"summary": "index",
"tags": [
"SecretItem"
],
"security": [
{
"SecretApiKeyAuth": [

]
}
],
"responses": {
"200": {
"description": "authorizes with secret key",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"items"
]
},
"example": {
"items": [
"secrets"
]
}
}
}
},
"401": {
"description": "authorizes with secret key",
"content": {
"text/html": {
"schema": {
"type": "string"
},
"example": ""
}
}
}
}
}
},
"/tables": {
"get": {
"summary": "index",
Expand Down Expand Up @@ -1035,5 +1089,14 @@
}
}
}
},
"components": {
"securitySchemes": {
"SecretApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "Secret-Key"
}
}
}
}
37 changes: 37 additions & 0 deletions spec/rails/doc/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,37 @@ paths:
schema:
type: string
example: ANOTHER TEST
"/secret_items":
get:
summary: index
tags:
- SecretItem
security:
- SecretApiKeyAuth: []
responses:
'200':
description: authorizes with secret key
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
type: string
required:
- items
example:
items:
- secrets
'401':
description: authorizes with secret key
content:
text/html:
schema:
type: string
example: ''
"/tables":
get:
summary: index
Expand Down Expand Up @@ -678,3 +709,9 @@ paths:
schema:
type: string
example: A TEST
components:
securitySchemes:
SecretApiKeyAuth:
type: apiKey
in: header
name: Secret-Key
24 changes: 23 additions & 1 deletion spec/requests/rails_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
require 'rspec/rails'

RSpec::OpenAPI.title = 'OpenAPI Documentation'
RSpec::OpenAPI.request_headers = %w[X-Authorization-Token]
RSpec::OpenAPI.request_headers = %w[X-Authorization-Token Secret-Key]
RSpec::OpenAPI.response_headers = %w[X-Cursor]
RSpec::OpenAPI.path = File.expand_path("../rails/doc/openapi.#{ENV.fetch('OPENAPI_OUTPUT', nil)}", __dir__)
RSpec::OpenAPI.comment = <<~COMMENT
Expand All @@ -26,6 +26,14 @@
},
}

RSpec::OpenAPI.security_schemes = {
SecretApiKeyAuth: {
type: 'apiKey',
in: 'header',
name: 'Secret-Key',
},
}

RSpec.describe 'Tables', type: :request do
describe '#index' do
context it 'returns a list of tables' do
Expand Down Expand Up @@ -198,6 +206,20 @@
end
end

RSpec.describe 'SecretKey securityScheme',
type: :request,
openapi: { security: [{ 'SecretApiKeyAuth' => [] }] } do
describe '#secret_items' do
it 'authorizes with secret key' do
get '/secret_items',
headers: {
'Secret-Key' => '42',
}
expect(response.status).to eq(200)
end
end
end

RSpec.describe 'Extra routes', type: :request do
describe '#test_block' do
it 'returns the block content' do
Expand Down
6 changes: 3 additions & 3 deletions spec/rspec/rails_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@
end

it 'generates the same spec/rails/doc/openapi.json' do
org_yaml = JSON.parse(File.read(openapi_path))
org_json = JSON.parse(File.read(openapi_path))
rspec 'spec/requests/rails_spec.rb', openapi: true, output: :json
new_yaml = JSON.parse(File.read(openapi_path))
expect(new_yaml).to eq org_yaml
new_json = JSON.parse(File.read(openapi_path))
expect(new_json).to eq org_json
end
end

Expand Down