Skip to content

Commit

Permalink
Add header schema validation
Browse files Browse the repository at this point in the history
  • Loading branch information
tzmfreedom committed Mar 15, 2018
1 parent 408eb9f commit bfcfcc1
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 126 deletions.
76 changes: 54 additions & 22 deletions lib/committee/drivers/open_api_2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,66 @@ class Link
# data.
attr_accessor :target_schema

attr_accessor :header_schema

def rel
raise "Committee: rel not implemented for OpenAPI"
end
end

# ParameterSchemaBuilder converts OpenAPI 2 link parameters, which are not
# quite JSON schemas (but will be in OpenAPI 3) into synthetic schemas that
# we can use to do some basic request validation.
class ParameterSchemaBuilder
class SchemaBuilder
def initialize(link_data)
self.link_data = link_data
end

private

LINK_REQUIRED_FIELDS = [
:name
].map(&:to_s).freeze

attr_accessor :link_data

def check_required_fields!(param_data)
LINK_REQUIRED_FIELDS.each do |field|
if !param_data[field]
raise ArgumentError,
"Committee: no #{field} section in link data."
end
end
end
end

class HeaderSchemaBuilder < SchemaBuilder
def call
if link_data["parameters"]
link_schema = JsonSchema::Schema.new
link_schema.properties = {}
link_schema.required = []

header_parameters = link_data["parameters"].select { |param_data| param_data["in"] == "header" }
header_parameters.each do |param_data|
check_required_fields!(param_data)

param_schema = JsonSchema::Schema.new

param_schema.type = [param_data["type"]]

link_schema.properties[param_data["name"]] = param_schema
if param_data["required"] == true
link_schema.required << param_data["name"]
end
end

link_schema
end
end
end

# ParameterSchemaBuilder converts OpenAPI 2 link parameters, which are not
# quite JSON schemas (but will be in OpenAPI 3) into synthetic schemas that
# we can use to do some basic request validation.
class ParameterSchemaBuilder < SchemaBuilder
# Returns a tuple of (schema, schema_data) where only one of the two
# values is present. This is either a full schema that's ready to go _or_
# a hash of unparsed schema data.
Expand All @@ -115,7 +162,8 @@ def call
link_schema.properties = {}
link_schema.required = []

link_data["parameters"].each do |param_data|
parameters = link_data["parameters"].reject { |param_data| param_data["in"] == "header" }
parameters.each do |param_data|
check_required_fields!(param_data)

param_schema = JsonSchema::Schema.new
Expand Down Expand Up @@ -143,23 +191,6 @@ def call
end
end
end

private

LINK_REQUIRED_FIELDS = [
:name
].map(&:to_s).freeze

attr_accessor :link_data

def check_required_fields!(param_data)
LINK_REQUIRED_FIELDS.each do |field|
if !param_data[field]
raise ArgumentError,
"Committee: no #{field} section in link data."
end
end
end
end

class Schema < Committee::Drivers::Schema
Expand Down Expand Up @@ -266,6 +297,7 @@ def parse_routes!(data, schema, store)
# Convert the spec's parameter pseudo-schemas into JSON schemas that
# we can use for some basic request validation.
link.schema, schema_data = ParameterSchemaBuilder.new(link_data).call
link.header_schema = HeaderSchemaBuilder.new(link_data).call

# If data came back instead of a schema (this occurs when a route has
# a single `body` parameter instead of a collection of URL/query/form
Expand Down
1 change: 1 addition & 0 deletions lib/committee/middleware/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ def initialize(app, options={})

@error_class = options.fetch(:error_class, Committee::ValidationError)
@params_key = options[:params_key] || "committee.params"
@headers_key = options[:headers_key] || "committee.headers"
@raise = options[:raise]
@schema = get_schema(options[:schema] ||
raise(ArgumentError, "Committee: need option `schema`"))
Expand Down
7 changes: 4 additions & 3 deletions lib/committee/middleware/request_validation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ def initialize(app, options={})
@allow_form_params = options.fetch(:allow_form_params, true)
@allow_query_params = options.fetch(:allow_query_params, true)
@check_content_type = options.fetch(:check_content_type, true)
@check_header = options.fetch(:check_header, true)
@optimistic_json = options.fetch(:optimistic_json, false)
@strict = options[:strict]
@coerce_date_times = options.fetch(:coerce_date_times, false)
Expand Down Expand Up @@ -39,7 +40,7 @@ def handle(request)
end
end

request.env[@params_key] = Committee::RequestUnpacker.new(
request.env[@params_key], request.env[@headers_key] = Committee::RequestUnpacker.new(
request,
allow_form_params: @allow_form_params,
allow_query_params: @allow_query_params,
Expand All @@ -51,8 +52,8 @@ def handle(request)
request.env[@params_key].merge!(param_matches) if param_matches

if link
validator = Committee::RequestValidator.new(link, check_content_type: @check_content_type)
validator.call(request, request.env[@params_key])
validator = Committee::RequestValidator.new(link, check_content_type: @check_content_type, check_header: @check_header)
validator.call(request, request.env[@params_key], request.env[@headers_key])

parameter_coerce!(request, link, @params_key)
parameter_coerce!(request, link, "rack.request.query_hash") if !request.GET.nil? && !link.schema.nil?
Expand Down
13 changes: 11 additions & 2 deletions lib/committee/request_unpacker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ def call
end

if @allow_query_params
indifferent_params(@request.GET).merge(params)
[indifferent_params(@request.GET).merge(params), headers]
else
params
[params, headers]
end
end

Expand Down Expand Up @@ -87,5 +87,14 @@ def parse_json
nil
end
end

def headers
env = @request.env
env.keys.grep(/HTTP_/).inject({}) do |headers, key|
headerized_key = key.gsub(/^HTTP_/, '').gsub(/_/, '-')
headers[headerized_key] = env[key]
headers
end
end
end
end
15 changes: 12 additions & 3 deletions lib/committee/request_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@ class RequestValidator
def initialize(link, options = {})
@link = link
@check_content_type = options.fetch(:check_content_type, true)
@check_header = options.fetch(:check_header, true)
end

def call(request, data)
check_content_type!(request, data) if @check_content_type
def call(request, params, headers)
check_content_type!(request, params) if @check_content_type
if @link.schema
valid, errors = @link.schema.validate(data)
valid, errors = @link.schema.validate(params)
if !valid
errors = JsonSchema::SchemaError.aggregate(errors).join("\n")
raise InvalidRequest, "Invalid request.\n\n#{errors}"
end
end

if @check_header && @link.respond_to?(:header_schema) && @link.header_schema
valid, errors = @link.header_schema.validate(headers)
if !valid
errors = JsonSchema::SchemaError.aggregate(errors).join("\n")
raise InvalidRequest, "Invalid request.\n\n#{errors}"
Expand Down
7 changes: 7 additions & 0 deletions test/data/openapi2/petstore-expanded.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
"required": false,
"type": "integer",
"format": "int32"
},
{
"name": "AUTH-TOKEN",
"in": "header",
"description": "authorization token",
"required": true,
"type": "string"
}
],
"responses": {
Expand Down
23 changes: 23 additions & 0 deletions test/drivers/open_api_2_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,26 @@ def call(data)
Committee::Drivers::OpenAPI2::ParameterSchemaBuilder.new(data).call
end
end

describe Committee::Drivers::OpenAPI2::HeaderSchemaBuilder do
it "returns schema data for header" do
data = {
"parameters" => [
{
"name" => "AUTH_TOKEN",
"type" => "string",
"in" => "header",
}
]
}
schema = call(data)

assert_equal ["string"], schema.properties["AUTH_TOKEN"].type
end

private

def call(data)
Committee::Drivers::OpenAPI2::HeaderSchemaBuilder.new(data).call
end
end
4 changes: 2 additions & 2 deletions test/middleware/request_validation_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -347,13 +347,13 @@ def app
}

@app = new_rack_app_with_lambda(check_parameter, schema: open_api_2_schema)
get "/api/pets?limit=3"
get "/api/pets?limit=3", nil, { "HTTP_AUTH_TOKEN" => "xxx" }
assert_equal 200, last_response.status
end

it "detects an invalid request for OpenAPI" do
@app = new_rack_app(schema: open_api_2_schema)
get "/api/pets?limit=foo"
get "/api/pets?limit=foo", nil, { "HTTP_AUTH_TOKEN" => "xxx" }
assert_equal 400, last_response.status
assert_match /invalid request/i, last_response.body
end
Expand Down
36 changes: 23 additions & 13 deletions test/request_unpacker_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"rack.input" => StringIO.new('{"x":"y"}'),
}
request = Rack::Request.new(env)
params = Committee::RequestUnpacker.new(request).call
params, _ = Committee::RequestUnpacker.new(request).call
assert_equal({ "x" => "y" }, params)
end

Expand All @@ -18,7 +18,7 @@
"rack.input" => StringIO.new('{"x":"y"}'),
}
request = Rack::Request.new(env)
params = Committee::RequestUnpacker.new(request).call
params, _ = Committee::RequestUnpacker.new(request).call
assert_equal({ "x" => "y" }, params)
end

Expand All @@ -28,7 +28,7 @@
"rack.input" => StringIO.new('{"x":"y"}'),
}
request = Rack::Request.new(env)
params = Committee::RequestUnpacker.new(request).call
params, _ = Committee::RequestUnpacker.new(request).call
assert_equal({}, params)
end

Expand All @@ -38,7 +38,7 @@
"rack.input" => StringIO.new('{"x":"y"}'),
}
request = Rack::Request.new(env)
params = Committee::RequestUnpacker.new(request, optimistic_json: true).call
params, _ = Committee::RequestUnpacker.new(request, optimistic_json: true).call
assert_equal({ "x" => "y" }, params)
end

Expand All @@ -48,7 +48,7 @@
"rack.input" => StringIO.new('x=y&foo=42'),
}
request = Rack::Request.new(env)
params = Committee::RequestUnpacker.new(request, optimistic_json: true).call
params, _ = Committee::RequestUnpacker.new(request, optimistic_json: true).call
assert_equal({}, params)
end

Expand All @@ -58,7 +58,7 @@
"rack.input" => StringIO.new(""),
}
request = Rack::Request.new(env)
params = Committee::RequestUnpacker.new(request).call
params, _ = Committee::RequestUnpacker.new(request).call
assert_equal({}, params)
end

Expand All @@ -68,7 +68,7 @@
"rack.input" => StringIO.new("x=y"),
}
request = Rack::Request.new(env)
params = Committee::RequestUnpacker.new(request).call
params, _ = Committee::RequestUnpacker.new(request).call
assert_equal({}, params)
end

Expand All @@ -78,7 +78,7 @@
"rack.input" => StringIO.new("x=y"),
}
request = Rack::Request.new(env)
params = Committee::RequestUnpacker.new(request, allow_form_params: true).call
params, _ = Committee::RequestUnpacker.new(request, allow_form_params: true).call
assert_equal({ "x" => "y" }, params)
end

Expand All @@ -92,7 +92,7 @@
"rack.input" => StringIO.new("x=1"),
}
request = Rack::Request.new(env)
params = Committee::RequestUnpacker.new(
params, _ = Committee::RequestUnpacker.new(
request,
allow_form_params: true,
coerce_form_params: true,
Expand All @@ -108,7 +108,7 @@
"QUERY_STRING" => "a=b"
}
request = Rack::Request.new(env)
params = Committee::RequestUnpacker.new(request, allow_form_params: true, allow_query_params: true).call
params, _ = Committee::RequestUnpacker.new(request, allow_form_params: true, allow_query_params: true).call
assert_equal({ "x" => "y", "a" => "b" }, params)
end

Expand All @@ -118,7 +118,7 @@
"QUERY_STRING" => "a=b"
}
request = Rack::Request.new(env)
params = Committee::RequestUnpacker.new(request, allow_query_params: true).call
params, _ = Committee::RequestUnpacker.new(request, allow_query_params: true).call
assert_equal({ "a" => "b" }, params)
end

Expand All @@ -139,7 +139,7 @@
"rack.input" => StringIO.new('{"x":"y"}'),
}
request = Rack::Request.new(env)
params = Committee::RequestUnpacker.new(request).call
params, _ = Committee::RequestUnpacker.new(request).call
assert_equal({}, params)
end

Expand All @@ -149,7 +149,17 @@
"rack.input" => StringIO.new('{"x":[]}'),
}
request = Rack::Request.new(env)
params = Committee::RequestUnpacker.new(request).call
params, _ = Committee::RequestUnpacker.new(request).call
assert_equal({ "x" => [] }, params)
end

it "unpacks http header" do
env = {
"HTTP_FOO_BAR" => "some header value",
"rack.input" => StringIO.new(""),
}
request = Rack::Request.new(env)
_, headers = Committee::RequestUnpacker.new(request, { allow_header_params: true }).call
assert_equal({ "FOO-BAR" => "some header value" }, headers)
end
end
Loading

0 comments on commit bfcfcc1

Please sign in to comment.