Merge pull request #71 from domaindrivendev/basic-auth-take2

Basic auth take2
This commit is contained in:
domaindrivendev 2017-06-28 00:16:38 -07:00 committed by GitHub
commit 519eb74cdd
11 changed files with 209 additions and 34 deletions

View File

@ -81,24 +81,24 @@ GEM
railties (>= 3.0.0) railties (>= 3.0.0)
globalid (0.4.0) globalid (0.4.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
i18n (0.8.1) i18n (0.8.4)
json (1.8.6) json (1.8.6)
json-schema (2.8.0) json-schema (2.8.0)
addressable (>= 2.4) addressable (>= 2.4)
libv8 (3.16.14.15) libv8 (3.16.14.15)
loofah (2.0.3) loofah (2.0.3)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.6.5) mail (2.6.6)
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
method_source (0.8.2) method_source (0.8.2)
mime-types (3.1) mime-types (3.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
mini_portile2 (2.1.0) mini_portile2 (2.2.0)
minitest (5.10.2) minitest (5.10.2)
nio4r (2.0.0) nio4r (2.1.0)
nokogiri (1.6.8.1) nokogiri (1.8.0)
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.2.0)
power_assert (0.3.1) power_assert (0.3.1)
rack (2.0.3) rack (2.0.3)
rack-test (0.6.3) rack-test (0.6.3)

View File

@ -200,6 +200,58 @@ describe 'Blogs API', swagger_doc: 'v2/swagger.json' do
end 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 ## ## 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. 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.

View File

@ -14,7 +14,7 @@ module Rswag
api_metadata[:operation][:verb], api_metadata[:operation][:verb],
factory.build_fullpath(self), factory.build_fullpath(self),
factory.build_body(self), factory.build_body(self),
factory.build_headers(self) rackify_headers(factory.build_headers(self)) # Rails test infrastructure requires Rack headers
) )
else else
send( send(
@ -36,6 +36,22 @@ module Rswag
private 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 def rswag_config
::Rswag::Specs.config ::Rswag::Specs.config
end end

View File

@ -32,13 +32,20 @@ module Rswag
end end
def build_headers(example) def build_headers(example)
headers = Hash[ parameters_in(:header).map { |p| [ p[:name], example.send(p[:name]).to_s ] } ] name_value_pairs = parameters_in(:header).map do |param|
headers.tap do |h| [
produces = @api_metadata[:operation][:produces] || @global_metadata[:produces] param[:name],
consumes = @api_metadata[:operation][:consumes] || @global_metadata[:consumes] example.send(param[:name]).to_s
h['ACCEPT'] = produces.join(';') unless produces.nil? ]
h['CONTENT_TYPE'] = consumes.join(';') unless consumes.nil?
end 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 end
private private
@ -52,42 +59,48 @@ module Rswag
applicable_params applicable_params
.map { |p| p['$ref'] ? resolve_parameter(p['$ref']) : p } # resolve any references .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 } .select { |p| p[:in] == location }
end end
def resolve_parameter(ref) def resolve_parameter(ref)
defined_params = @global_metadata[:parameters] defined_params = @global_metadata[:parameters]
key = ref.sub('#/parameters/', '') key = ref.sub('#/parameters/', '')
raise "Referenced parameter '#{ref}' must be defined" unless defined_params && defined_params[key] raise "Referenced parameter '#{ref}' must be defined" unless defined_params && defined_params[key]
defined_params[key] defined_params[key]
end end
def resolve_api_key_parameters def security_parameters
@api_key_params ||= begin applicable_security_schemes.map do |scheme|
# First figure out the security requirement applicable to the operation if scheme[:type] == :apiKey
global_requirements = (@global_metadata[:security] || [] ).map { |r| r.keys.first } { name: scheme[:name], type: :string, in: scheme[:in] }
operation_requirements = (@api_metadata[:operation][:security] || [] ).map { |r| r.keys.first } else
requirements = global_requirements | operation_requirements { name: 'Authorization', type: :string, in: :header } # use auth header for basic & oauth2
end
# Then obtain the scheme definitions for those requirements
definitions = (@global_metadata[:securityDefinitions] || {}).slice(*requirements)
definitions.values.select { |d| d[:type] == :apiKey }
end 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) def build_query_string_part(param, value)
return "#{param[:name]}=#{value.to_s}" unless param[:type].to_sym == :array return "#{param[:name]}=#{value.to_s}" unless param[:type].to_sym == :array
name = param[:name] name = param[:name]
case param[:collectionFormat] case param[:collectionFormat]
when :ssv when :ssv
"#{name}=#{value.join(' ')}" "#{name}=#{value.join(' ')}"
when :tsv when :tsv
"#{name}=#{value.join('\t')}" "#{name}=#{value.join('\t')}"
when :pipes when :pipes
"#{name}=#{value.join('|')}" "#{name}=#{value.join('|')}"
when :multi when :multi
value.map { |v| "#{name}=#{v}" }.join('&') value.map { |v| "#{name}=#{v}" }.join('&')
else else
"#{name}=#{value.join(',')}" # csv is default "#{name}=#{value.join(',')}" # csv is default

View File

@ -193,6 +193,33 @@ module Rswag
end end
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 context 'consumes & produces' do
before do before do
api_metadata[:operation][:consumes] = [ 'application/json', 'application/xml' ] api_metadata[:operation][:consumes] = [ 'application/json', 'application/xml' ]
@ -201,8 +228,8 @@ module Rswag
it "includes corresponding 'Accept' & 'Content-Type' headers" do it "includes corresponding 'Accept' & 'Content-Type' headers" do
expect(headers).to match( expect(headers).to match(
'ACCEPT' => 'application/json;application/xml', 'Accept' => 'application/json;application/xml',
'CONTENT_TYPE' => 'application/json;application/xml' 'Content-Type' => 'application/json;application/xml'
) )
end end
end end
@ -217,8 +244,8 @@ module Rswag
it "includes corresponding 'Accept' & 'Content-Type' headers" do it "includes corresponding 'Accept' & 'Content-Type' headers" do
expect(headers).to match( expect(headers).to match(
'ACCEPT' => 'application/json;application/xml', 'Accept' => 'application/json;application/xml',
'CONTENT_TYPE' => 'application/json;application/xml' 'Content-Type' => 'application/json;application/xml'
) )
end end
end end

View File

@ -1,7 +1,7 @@
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception. # Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead. # 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 ] wrap_parameters format: [ :json ]
end end

View 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

View File

@ -1,6 +1,8 @@
TestApp::Application.routes.draw do TestApp::Application.routes.draw do
resources :blogs, defaults: { :format => :json } resources :blogs, defaults: { :format => :json }
post 'auth-tests/basic', to: 'auth_tests#basic'
mount Rswag::Api::Engine => 'api-docs' mount Rswag::Api::Engine => 'api-docs'
mount Rswag::Ui::Engine => 'api-docs' mount Rswag::Ui::Engine => 'api-docs'
end end

View 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

View File

@ -45,6 +45,9 @@ RSpec.configure do |config|
} }
}, },
securityDefinitions: { securityDefinitions: {
basic_auth: {
type: :basic
},
api_key: { api_key: {
type: :apiKey, type: :apiKey,
name: 'api_key', name: 'api_key',

View File

@ -5,6 +5,30 @@
"version": "v1" "version": "v1"
}, },
"paths": { "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": { "/blogs": {
"post": { "post": {
"summary": "Creates a blog", "summary": "Creates a blog",
@ -158,6 +182,9 @@
} }
}, },
"securityDefinitions": { "securityDefinitions": {
"basic_auth": {
"type": "basic"
},
"api_key": { "api_key": {
"type": "apiKey", "type": "apiKey",
"name": "api_key", "name": "api_key",