mirror of
https://github.com/ditkrg/rswag.git
synced 2026-01-23 06:16:42 +00:00
Leverage security definitions for headers in example requests
This commit is contained in:
parent
e40c5fc26e
commit
de7ec5f15d
12
Gemfile.lock
12
Gemfile.lock
@ -81,24 +81,24 @@ GEM
|
||||
railties (>= 3.0.0)
|
||||
globalid (0.4.0)
|
||||
activesupport (>= 4.2.0)
|
||||
i18n (0.8.1)
|
||||
i18n (0.8.4)
|
||||
json (1.8.6)
|
||||
json-schema (2.8.0)
|
||||
addressable (>= 2.4)
|
||||
libv8 (3.16.14.15)
|
||||
loofah (2.0.3)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.6.5)
|
||||
mail (2.6.6)
|
||||
mime-types (>= 1.16, < 4)
|
||||
method_source (0.8.2)
|
||||
mime-types (3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mini_portile2 (2.1.0)
|
||||
mini_portile2 (2.2.0)
|
||||
minitest (5.10.2)
|
||||
nio4r (2.0.0)
|
||||
nokogiri (1.6.8.1)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
nio4r (2.1.0)
|
||||
nokogiri (1.8.0)
|
||||
mini_portile2 (~> 2.2.0)
|
||||
power_assert (0.3.1)
|
||||
rack (2.0.3)
|
||||
rack-test (0.6.3)
|
||||
|
||||
52
README.md
52
README.md
@ -200,6 +200,58 @@ describe 'Blogs API', swagger_doc: 'v2/swagger.json' do
|
||||
end
|
||||
```
|
||||
|
||||
### Specifying/Testing API Security ###
|
||||
|
||||
Swagger allows for the specification of different security schemes and their applicability to operations in an API. To leverage this in rswag, you define the schemes globally in _swagger_helper.rb_ and then use the "security" attribute at the operation level to specify which schemes, if any, are applicable to that operation. Swagger supports :basic, :apiKey and :oauth2 scheme types. See [the spec](http://swagger.io/specification/#security-definitions-object-109) for more info.
|
||||
|
||||
```ruby
|
||||
# spec/swagger_helper.rb
|
||||
RSpec.configure do |config|
|
||||
config.swagger_root = Rails.root.to_s + '/swagger'
|
||||
|
||||
config.swagger_docs = {
|
||||
'v1/swagger.json' => {
|
||||
...
|
||||
securityDefinitions: {
|
||||
basic: {
|
||||
type: :basic
|
||||
},
|
||||
apiKey: {
|
||||
type: :apiKey,
|
||||
name: 'api_key',
|
||||
in: :query
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
# spec/integration/blogs_spec.rb
|
||||
describe 'Blogs API' do
|
||||
|
||||
path '/blogs' do
|
||||
|
||||
post 'Creates a blog' do
|
||||
tags 'Blogs'
|
||||
security [ basic: [] ]
|
||||
...
|
||||
|
||||
response '201', 'blog created' do
|
||||
let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" }
|
||||
run_test!
|
||||
end
|
||||
|
||||
response '401', 'authentication failed' do
|
||||
let(:Authorization) { "Basic #{::Base64.strict_encode64('bogus:bogus')}" }
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
__NOTE:__ Depending on the scheme types, you'll be required to assign a corresponding parameter value with each example. For example, :basic auth is required above and so the :Authorization (header) parameter must be set accordingly
|
||||
|
||||
## Configuration & Customization ##
|
||||
|
||||
The steps described above will get you up and running with minimal setup. However, rswag offers a lot of flexibility to customize as you see fit. Before exploring the various options, you'll need to be aware of it's different components. The following table lists each of them and the files that get added/updated as part of a standard install.
|
||||
|
||||
@ -14,7 +14,7 @@ module Rswag
|
||||
api_metadata[:operation][:verb],
|
||||
factory.build_fullpath(self),
|
||||
factory.build_body(self),
|
||||
factory.build_headers(self)
|
||||
rackify_headers(factory.build_headers(self)) # Rails test infrastructure requires Rack headers
|
||||
)
|
||||
else
|
||||
send(
|
||||
@ -36,6 +36,22 @@ module Rswag
|
||||
|
||||
private
|
||||
|
||||
def rackify_headers(headers)
|
||||
name_value_pairs = headers.map do |name, value|
|
||||
[
|
||||
case name
|
||||
when 'Accept' then 'HTTP_ACCEPT'
|
||||
when 'Content-Type' then 'CONTENT_TYPE'
|
||||
when 'Authorization' then 'HTTP_AUTHORIZATION'
|
||||
else name
|
||||
end,
|
||||
value
|
||||
]
|
||||
end
|
||||
|
||||
Hash[ name_value_pairs ]
|
||||
end
|
||||
|
||||
def rswag_config
|
||||
::Rswag::Specs.config
|
||||
end
|
||||
|
||||
@ -32,13 +32,20 @@ module Rswag
|
||||
end
|
||||
|
||||
def build_headers(example)
|
||||
headers = Hash[ parameters_in(:header).map { |p| [ p[:name], example.send(p[:name]).to_s ] } ]
|
||||
headers.tap do |h|
|
||||
produces = @api_metadata[:operation][:produces] || @global_metadata[:produces]
|
||||
consumes = @api_metadata[:operation][:consumes] || @global_metadata[:consumes]
|
||||
h['ACCEPT'] = produces.join(';') unless produces.nil?
|
||||
h['CONTENT_TYPE'] = consumes.join(';') unless consumes.nil?
|
||||
name_value_pairs = parameters_in(:header).map do |param|
|
||||
[
|
||||
param[:name],
|
||||
example.send(param[:name]).to_s
|
||||
]
|
||||
end
|
||||
|
||||
# Add MIME type headers based on produces/consumes metadata
|
||||
produces = @api_metadata[:operation][:produces] || @global_metadata[:produces]
|
||||
consumes = @api_metadata[:operation][:consumes] || @global_metadata[:consumes]
|
||||
name_value_pairs << [ 'Accept', produces.join(';') ] unless produces.nil?
|
||||
name_value_pairs << [ 'Content-Type', consumes.join(';') ] unless consumes.nil?
|
||||
|
||||
Hash[ name_value_pairs ]
|
||||
end
|
||||
|
||||
private
|
||||
@ -52,7 +59,7 @@ module Rswag
|
||||
|
||||
applicable_params
|
||||
.map { |p| p['$ref'] ? resolve_parameter(p['$ref']) : p } # resolve any references
|
||||
.concat(resolve_api_key_parameters)
|
||||
.concat(security_parameters)
|
||||
.select { |p| p[:in] == location }
|
||||
end
|
||||
|
||||
@ -63,19 +70,25 @@ module Rswag
|
||||
defined_params[key]
|
||||
end
|
||||
|
||||
def resolve_api_key_parameters
|
||||
@api_key_params ||= begin
|
||||
# First figure out the security requirement applicable to the operation
|
||||
global_requirements = (@global_metadata[:security] || [] ).map { |r| r.keys.first }
|
||||
operation_requirements = (@api_metadata[:operation][:security] || [] ).map { |r| r.keys.first }
|
||||
requirements = global_requirements | operation_requirements
|
||||
|
||||
# Then obtain the scheme definitions for those requirements
|
||||
definitions = (@global_metadata[:securityDefinitions] || {}).slice(*requirements)
|
||||
definitions.values.select { |d| d[:type] == :apiKey }
|
||||
def security_parameters
|
||||
applicable_security_schemes.map do |scheme|
|
||||
if scheme[:type] == :apiKey
|
||||
{ name: scheme[:name], type: :string, in: scheme[:in] }
|
||||
else
|
||||
{ name: 'Authorization', type: :string, in: :header } # use auth header for basic & oauth2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def applicable_security_schemes
|
||||
# First figure out the security requirement applicable to the operation
|
||||
requirements = @api_metadata[:operation][:security] || @global_metadata[:security]
|
||||
scheme_names = requirements ? requirements.map { |r| r.keys.first } : []
|
||||
|
||||
# Then obtain the scheme definitions for those requirements
|
||||
(@global_metadata[:securityDefinitions] || {}).slice(*scheme_names).values
|
||||
end
|
||||
|
||||
def build_query_string_part(param, value)
|
||||
return "#{param[:name]}=#{value.to_s}" unless param[:type].to_sym == :array
|
||||
|
||||
|
||||
@ -193,6 +193,33 @@ module Rswag
|
||||
end
|
||||
end
|
||||
|
||||
context "global definition for 'basic auth'" do
|
||||
before do
|
||||
global_metadata[:securityDefinitions] = { basic_auth: { type: :basic} }
|
||||
allow(example).to receive(:'Authorization').and_return('Basic foobar')
|
||||
end
|
||||
|
||||
context 'global requirement' do
|
||||
before { global_metadata[:security] = [ { basic_auth: [] } ] }
|
||||
|
||||
it "includes a corresponding Authorization header" do
|
||||
expect(headers).to match(
|
||||
'Authorization' => 'Basic foobar'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'operation-specific requirement' do
|
||||
before { api_metadata[:operation][:security] = [ { basic_auth: [] } ] }
|
||||
|
||||
it "includes a corresponding Authorization header" do
|
||||
expect(headers).to match(
|
||||
'Authorization' => 'Basic foobar'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'consumes & produces' do
|
||||
before do
|
||||
api_metadata[:operation][:consumes] = [ 'application/json', 'application/xml' ]
|
||||
@ -201,8 +228,8 @@ module Rswag
|
||||
|
||||
it "includes corresponding 'Accept' & 'Content-Type' headers" do
|
||||
expect(headers).to match(
|
||||
'ACCEPT' => 'application/json;application/xml',
|
||||
'CONTENT_TYPE' => 'application/json;application/xml'
|
||||
'Accept' => 'application/json;application/xml',
|
||||
'Content-Type' => 'application/json;application/xml'
|
||||
)
|
||||
end
|
||||
end
|
||||
@ -217,8 +244,8 @@ module Rswag
|
||||
|
||||
it "includes corresponding 'Accept' & 'Content-Type' headers" do
|
||||
expect(headers).to match(
|
||||
'ACCEPT' => 'application/json;application/xml',
|
||||
'CONTENT_TYPE' => 'application/json;application/xml'
|
||||
'Accept' => 'application/json;application/xml',
|
||||
'Content-Type' => 'application/json;application/xml'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
# Prevent CSRF attacks by raising an exception.
|
||||
# For APIs, you may want to use :null_session instead.
|
||||
protect_from_forgery with: :exception
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
wrap_parameters format: [ :json ]
|
||||
end
|
||||
|
||||
13
test-app/app/controllers/auth_tests_controller.rb
Normal file
13
test-app/app/controllers/auth_tests_controller.rb
Normal file
@ -0,0 +1,13 @@
|
||||
class AuthTestsController < ApplicationController
|
||||
wrap_parameters Blog
|
||||
respond_to :json
|
||||
|
||||
# POST /auth-tests/basic
|
||||
def basic
|
||||
if authenticate_with_http_basic { |u, p| u == 'jsmith' && p == 'jspass' }
|
||||
head :no_content
|
||||
else
|
||||
request_http_basic_authentication
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -1,6 +1,8 @@
|
||||
TestApp::Application.routes.draw do
|
||||
resources :blogs, defaults: { :format => :json }
|
||||
|
||||
post 'auth-tests/basic', to: 'auth_tests#basic'
|
||||
|
||||
mount Rswag::Api::Engine => 'api-docs'
|
||||
mount Rswag::Ui::Engine => 'api-docs'
|
||||
end
|
||||
|
||||
22
test-app/spec/integration/auth_tests_spec.rb
Normal file
22
test-app/spec/integration/auth_tests_spec.rb
Normal file
@ -0,0 +1,22 @@
|
||||
require 'swagger_helper'
|
||||
|
||||
describe 'Auth Tests API', type: :request, swagger_doc: 'v1/swagger.json' do
|
||||
|
||||
path '/auth-tests/basic' do
|
||||
post 'Authenticates with basic auth' do
|
||||
tags 'Auth Test'
|
||||
operationId 'testBasicAuth'
|
||||
security [ basic_auth: [] ]
|
||||
|
||||
response '204', 'Valid credentials' do
|
||||
let(:Authorization) { "Basic #{::Base64.strict_encode64('jsmith:jspass')}" }
|
||||
run_test!
|
||||
end
|
||||
|
||||
response '401', 'Invalid credentials' do
|
||||
let(:Authorization) { "Basic #{::Base64.strict_encode64('foo:bar')}" }
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -45,6 +45,9 @@ RSpec.configure do |config|
|
||||
}
|
||||
},
|
||||
securityDefinitions: {
|
||||
basic_auth: {
|
||||
type: :basic
|
||||
},
|
||||
api_key: {
|
||||
type: :apiKey,
|
||||
name: 'api_key',
|
||||
|
||||
@ -5,6 +5,30 @@
|
||||
"version": "v1"
|
||||
},
|
||||
"paths": {
|
||||
"/auth-tests/basic": {
|
||||
"post": {
|
||||
"summary": "Authenticates with basic auth",
|
||||
"tags": [
|
||||
"Auth Test"
|
||||
],
|
||||
"operationId": "testBasicAuth",
|
||||
"security": [
|
||||
{
|
||||
"basic_auth": [
|
||||
|
||||
]
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Valid credentials"
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/blogs": {
|
||||
"post": {
|
||||
"summary": "Creates a blog",
|
||||
@ -158,6 +182,9 @@
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"basic_auth": {
|
||||
"type": "basic"
|
||||
},
|
||||
"api_key": {
|
||||
"type": "apiKey",
|
||||
"name": "api_key",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user