From 7ecafe58012799048b64bd446298bd468139ed16 Mon Sep 17 00:00:00 2001 From: pablo Date: Wed, 7 Feb 2018 14:53:25 -0200 Subject: [PATCH] Add validation for header parameters --- lib/phoenix_swagger/plug/validate_plug.ex | 93 ++++++++++++++++--- test/plug/validate_test.exs | 16 ++++ test/test_spec/swagger_jsonapi_test_spec.json | 12 +++ 3 files changed, 108 insertions(+), 13 deletions(-) diff --git a/lib/phoenix_swagger/plug/validate_plug.ex b/lib/phoenix_swagger/plug/validate_plug.ex index 02a6a4e..4e183a4 100644 --- a/lib/phoenix_swagger/plug/validate_plug.ex +++ b/lib/phoenix_swagger/plug/validate_plug.ex @@ -19,27 +19,31 @@ defmodule PhoenixSwagger.Plug.Validate do result = with {:ok, path} <- find_matching_path(conn), :ok <- validate_body_params(path, conn), + :ok <- validate_header_params(path, conn), :ok <- validate_query_params(path, conn), do: {:ok, conn} case result do {:ok, conn} -> conn + {:error, :no_matching_path} -> send_error_response(conn, 404, "API does not provide resource", conn.request_path) + {:error, message, path} -> send_error_response(conn, validation_failed_status, message, path) end end defp find_matching_path(conn) do - found = Enum.find(:ets.tab2list(@table), fn({path, base_path, _}) -> - base_path_segments = String.split(base_path || "", "/") |> tl - path_segments = String.split(path, "/") |> tl - path_info_without_base = remove_base_path(conn.path_info, base_path_segments) - req_path_segments = [String.downcase(conn.method) | path_info_without_base] - equal_paths?(path_segments, req_path_segments) - end) + found = + Enum.find(:ets.tab2list(@table), fn {path, base_path, _} -> + base_path_segments = String.split(base_path || "", "/") |> tl + path_segments = String.split(path, "/") |> tl + path_info_without_base = remove_base_path(conn.path_info, base_path_segments) + req_path_segments = [String.downcase(conn.method) | path_info_without_base] + equal_paths?(path_segments, req_path_segments) + end) case found do nil -> {:error, :no_matching_path} @@ -64,6 +68,7 @@ defmodule PhoenixSwagger.Plug.Validate do defp validate_boolean(_name, value, parameters) when value in ["true", "false"] do validate_query_params(parameters) end + defp validate_boolean(name, _value, _parameters) do {:error, "Type mismatch. Expected Boolean but got String.", "#/#{name}"} end @@ -71,39 +76,50 @@ defmodule PhoenixSwagger.Plug.Validate do defp validate_integer(name, value, parameters) do _ = String.to_integer(value) validate_query_params(parameters) - rescue ArgumentError -> + rescue + ArgumentError -> {:error, "Type mismatch. Expected Integer but got String.", "#/#{name}"} end defp validate_query_params([]), do: :ok + defp validate_query_params([{_type, _name, nil, false} | parameters]) do validate_query_params(parameters) end + defp validate_query_params([{_type, name, nil, true} | _]) do {:error, "Required property #{name} was not present.", "#"} end + defp validate_query_params([{"string", _name, _val, _} | parameters]) do validate_query_params(parameters) end + defp validate_query_params([{"integer", name, val, _} | parameters]) do validate_integer(name, val, parameters) end + defp validate_query_params([{"boolean", name, val, _} | parameters]) do validate_boolean(name, val, parameters) end + defp validate_query_params(path, conn) do [{_path, _basePath, schema}] = :ets.lookup(@table, path) + parameters = for parameter <- schema.schema["parameters"], parameter["type"] != nil, parameter["in"] in ["query", "path"] do - {parameter["type"], parameter["name"], get_param_value(conn.params, parameter["name"]), parameter["required"]} + {parameter["type"], parameter["name"], get_param_value(conn.params, parameter["name"]), + parameter["required"]} end + validate_query_params(parameters) end defp get_in_nested(params = nil, _), do: params defp get_in_nested(params, nil), do: params + defp get_in_nested(params, nested_map) when map_size(nested_map) == 1 do [{key, child_nested_map}] = Map.to_list(nested_map) @@ -119,19 +135,70 @@ defmodule PhoenixSwagger.Plug.Validate do case Validator.validate(path, conn.body_params) do :ok -> :ok {:error, [{error, error_path} | _], _path} -> {:error, error, error_path} - {:error, error, error_path} -> {:error, error, error_path} + {:error, error, error_path} -> {:error, error, error_path} end end defp equal_paths?([], []), do: true - defp equal_paths?([head | orig_path_rest], [head | req_path_rest]), do: equal_paths?(orig_path_rest, req_path_rest) - defp equal_paths?(["{" <> _ | orig_path_rest], [_ | req_path_rest]), do: equal_paths?(orig_path_rest, req_path_rest) + + defp equal_paths?([head | orig_path_rest], [head | req_path_rest]), + do: equal_paths?(orig_path_rest, req_path_rest) + + defp equal_paths?(["{" <> _ | orig_path_rest], [_ | req_path_rest]), + do: equal_paths?(orig_path_rest, req_path_rest) + defp equal_paths?(_, _), do: false # It is pretty safe to strip request path by base path. They can't be # non-equal. In this way, the router even will not execute this plug. defp remove_base_path(path, []), do: path + defp remove_base_path([_path | rest], [_base_path | base_path_rest]) do remove_base_path(rest, base_path_rest) end -end + + defp validate_header_params([]), do: :ok + + defp validate_header_params([{_type, _name, nil, false} | parameters]) do + validate_header_params(parameters) + end + + defp validate_header_params([{_type, name, nil, true} | _]) do + {:error, "Required header #{name} was not present.", "#"} + end + + defp validate_header_params([{"string", _name, _val, _} | parameters]) do + validate_query_params(parameters) + end + + defp validate_header_params([{"integer", name, val, _} | parameters]) do + validate_integer(name, val, parameters) + end + + defp validate_header_params([{"boolean", name, val, _} | parameters]) do + validate_boolean(name, val, parameters) + end + + defp validate_header_params(path, conn) do + [{_path, _basePath, schema}] = :ets.lookup(@table, path) + + parameters = + for parameter <- schema.schema["parameters"], + parameter["type"] != nil, + parameter["in"] in ["header"] do + {parameter["type"], parameter["name"], + get_header_value(conn.req_headers, parameter["name"]), parameter["required"]} + end + + validate_header_params(parameters) + end + + defp get_header_value(headers, header_name) do + header_name_down_case = String.downcase(header_name) + + headers + |> Enum.find(fn {k, _v} -> + header_name_down_case == k + end) + end +end \ No newline at end of file diff --git a/test/plug/validate_test.exs b/test/plug/validate_test.exs index 88b81ff..8448a8d 100644 --- a/test/plug/validate_test.exs +++ b/test/plug/validate_test.exs @@ -22,6 +22,7 @@ defmodule PhoenixSwagger.Plug.ValidateTest do test "required param returns error when not present" do conn = :get |> conn("/shapes?filter[route]=Red") + |> put_req_header("request-id", "d92578b3-d281-48a8-9e91-32b276fe6458") |> parse() assert %Conn{halted: true, resp_body: body, status: 400} = Validate.call(conn, @opts) assert Poison.decode!(body) == %{ @@ -35,6 +36,7 @@ defmodule PhoenixSwagger.Plug.ValidateTest do test "required nested param returns error when not present" do conn = :get |> conn("/shapes?api_key=SECRET") + |> put_req_header("request-id", "d92578b3-d281-48a8-9e91-32b276fe6458") |> parse() assert %Conn{halted: true, resp_body: body, status: 400} = Validate.call(conn, @opts) assert Poison.decode!(body) == %{ @@ -45,9 +47,23 @@ defmodule PhoenixSwagger.Plug.ValidateTest do } end + test "required header returns error when not present" do + conn = :get + |> conn("/shapes?filter[route]=Red") + |> parse() + assert %Conn{halted: true, resp_body: body, status: 400} = Validate.call(conn, @opts) + assert Poison.decode!(body) == %{ + "error" => %{ + "message" => "Required header request-id was not present.", + "path" => "#" + } + } + end + test "does not halt when required params present" do conn = :get |> conn("/shapes?api_key=SECRET&filter[route]=Red") + |> put_req_header("request-id", "d92578b3-d281-48a8-9e91-32b276fe6458") |> parse() assert %Conn{halted: false} = Validate.call(conn, @opts) end diff --git a/test/test_spec/swagger_jsonapi_test_spec.json b/test/test_spec/swagger_jsonapi_test_spec.json index abf9fa6..c0f5805 100644 --- a/test/test_spec/swagger_jsonapi_test_spec.json +++ b/test/test_spec/swagger_jsonapi_test_spec.json @@ -70,6 +70,12 @@ "in": "query", "description": "Filter by `/data/{index}/relationships/route/data/id`. Multiple `/data/{index}/relationships/route/data/id` **MUST** be a comma-separated (U+002C COMMA, \",\") list." }, + { + "type": "string", + "required": false, + "name": "optional-header", + "in": "header" + }, { "type": "string", "required": false, @@ -80,6 +86,12 @@ "1" ], "description": "Filter by direction of travel along the route.\n\nThe meaning of `direction_id` varies based on the route. You can programmatically get the direction names from `/routes` `/data/{index}/attributes/direction_names` or `/routes/{id}` `/data/attriutes/direction_names`. The general pattern is as follows:\n\n| Route ID Pattern | `direction_id` | Direction Name |\n|------------------|----------------|----------------|\n| `Red` | `0` | `\"Southbound\"` |\n| `Red` | `1` | `\"Northbound\"` |\n| `Orange` | `0` | `\"Southbound\"` |\n| `Orange` | `1` | `\"Northbound\"` |\n| `Blue` | `0` | `\"Westbound\"` |\n| `Blue` | `1` | `\"Eastbound\"` |\n| `Green-*` | `0` | `\"Westbound\"` |\n| `Green-*` | `1` | `\"Eastbound\"` |\n| `*` | `0` | `\"Outbound\"` |\n| `*` | `1` | `\"Inbound\"` |\n\n\n\n\n" + }, + { + "type": "string", + "required": true, + "name": "request-id", + "in": "header" } ], "operationId": "Api.ShapeController.index",