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 @@ -32,6 +32,7 @@ def record_results!
RSpec::OpenAPI::SchemaCleaner.cleanup!(spec, new_from_zero)
RSpec::OpenAPI::ComponentsUpdater.update!(spec, new_from_zero)
RSpec::OpenAPI::SchemaCleaner.cleanup_empty_required_array!(spec)
RSpec::OpenAPI::SchemaCleaner.cleanup_conflicting_security_parameters!(spec)
RSpec::OpenAPI::SchemaSorter.deep_sort!(spec)
end
end
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
15 changes: 15 additions & 0 deletions spec/rails/app/controllers/secret_items_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class SecretItemsController < ApplicationController
before_action :authenticate_api_key!

def index
render json: { items: ['secrets'] }
end

private

def authenticate_api_key!
if request.env['Secret-Key'] != '42'
head :unauthorized
end
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
35 changes: 35 additions & 0 deletions spec/rails/doc/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,32 @@
}
}
},
"/secret_items": {
"get": {
"summary": "index",
"tags": [
"SecretItem"
],
"security": [
{
"SecretApiKeyAuth": []
}
],
"responses": {
"401": {
"description": "authorizes with secret key",
"content": {
"text/html": {
"schema": {
"type": "string"
},
"example": ""
}
}
}
}
}
},
"/tables": {
"get": {
"summary": "index",
Expand Down Expand Up @@ -1035,5 +1061,14 @@
}
}
}
},
"components": {
"securitySchemes": {
"SecretApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "Secret-Key"
}
}
}
}
21 changes: 21 additions & 0 deletions spec/rails/doc/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,21 @@ paths:
schema:
type: string
example: ANOTHER TEST
"/secret_items":
get:
summary: "index",
tags:
- "SecretItem"
security:
- SecretApiKeyAuth: []
responses:
401:
description: "authorizes with secret key",
content:
text/html:
schema:
type: "string"
example: ""
"/tables":
get:
summary: index
Expand Down Expand Up @@ -678,3 +693,9 @@ paths:
schema:
type: string
example: A TEST
components:
securitySchemes:
SecretApiKeyAuth:
type: apiKey
in: header
name: Secret-Key
21 changes: 21 additions & 0 deletions spec/requests/rails_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,19 @@
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',
}
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).tap {|j| puts j; puts j.inspect })
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
expect(new_json).to eq org_json
end
end

Expand Down
Loading