mirror of
https://github.com/ditkrg/rswag.git
synced 2026-01-22 22:06:43 +00:00
Merge pull request #71 from domaindrivendev/basic-auth-take2
Basic auth take2
This commit is contained in:
commit
519eb74cdd
12
Gemfile.lock
12
Gemfile.lock
@ -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)
|
||||||
|
|||||||
52
README.md
52
README.md
@ -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.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
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
|
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
|
||||||
|
|||||||
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: {
|
securityDefinitions: {
|
||||||
|
basic_auth: {
|
||||||
|
type: :basic
|
||||||
|
},
|
||||||
api_key: {
|
api_key: {
|
||||||
type: :apiKey,
|
type: :apiKey,
|
||||||
name: 'api_key',
|
name: 'api_key',
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user