From fcedcf5f95955b020daed46d99c3b0007268c52c Mon Sep 17 00:00:00 2001 From: Kevin Bader Date: Mon, 9 Sep 2019 00:11:35 +0200 Subject: [PATCH 1/7] Add blacklist i9n test and fix some stuff along the way Added: - When terminating an SSE connection after its associated session has been blacklisted, RIG now sends out a `rig.session_killed` event before closing the socket. Changed: - When a session has been added to the session blacklist successfully, the endpoint now uses the correct HTTP status code "201 Created" instead of "200 Ok". - When using the API to blacklist a session, the `validityInSeconds` should now be passed as an integer value (see `Deprecated` below). Deprecated: - When using the API to blacklist a session, passing the `validityInSeconds` field as a string is deprecated (but supported until the 3.0 release). Please use an integer instead. Removed: - Removed the `JWT_BLACKLIST_DEFAULT_EXPIRY_HOURS` environment variable ([deprecated since 2.0.0-beta.2]). Security: - A connection is now associated to its session right after the connection is established, given the request carries a JWT in its authorization header. Previously, this was only done by the subscriptions endpoint, which could cause a connection to remain active even after blacklisting its authorization token. [deprecated since 2.0.0-beta.2]: https://github.com/Accenture/reactive-interaction-gateway/commit/f974533455aa3ebc550ee95bf291585925a406d5 --- CHANGELOG.md | 10 +- lib/rig/jwt.ex | 2 +- lib/rig/session/connection.ex | 14 +-- .../session_blacklist_controller.ex | 76 ++++++++---- .../connection_init.ex | 12 +- lib/rig_inbound_gateway_web/v1/sse.ex | 47 +++++-- lib/rig_inbound_gateway_web/v1/websocket.ex | 23 +++- test/blacklist_test.exs | 107 ++++++++++++++++ test/connection_test.exs | 65 ++-------- test/support/clients.ex | 115 ++++++++++++++---- 10 files changed, 344 insertions(+), 127 deletions(-) create mode 100644 test/blacklist_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8025b1fe..95efea02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added longpolling as new connection type [#217](https://github.com/Accenture/reactive-interaction-gateway/issues/217) +- When terminating an SSE connection after its associated session has been blacklisted, RIG now sends out a `rig.session_killed` event before closing the socket. - +### Changed + +- When a session has been added to the session blacklist successfully, the endpoint now uses the correct HTTP status code "201 Created" instead of "200 Ok". +- When using the API to blacklist a session, the `validityInSeconds` should now be passed as an integer value (see `Deprecated` below). ### Fixed @@ -19,7 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Logging incoming HTTP request to Kafka works again and now also supports Apache Avro. [#170](https://github.com/Accenture/reactive-interaction-gateway/issues/170) - +### Deprecated + +- When using the API to blacklist a session, passing the `validityInSeconds` field as a string is deprecated (but supported until the 3.0 release). Please use an integer instead. ### Removed diff --git a/lib/rig/jwt.ex b/lib/rig/jwt.ex index b8f90942..0980b3ed 100644 --- a/lib/rig/jwt.ex +++ b/lib/rig/jwt.ex @@ -103,7 +103,7 @@ defmodule RIG.JWT do defp ensure_not_blacklisted(%{"jti" => jti} = claims) do if Session.blacklisted?(jti) do - {:error, "Ignoring blacklisted JWT with ID #{jti}."} + {:error, "Ignoring blacklisted JWT with ID #{inspect(jti)}."} else {:ok, claims} end diff --git a/lib/rig/session/connection.ex b/lib/rig/session/connection.ex index 18c54388..dfa5c681 100644 --- a/lib/rig/session/connection.ex +++ b/lib/rig/session/connection.ex @@ -7,9 +7,9 @@ defmodule RIG.Session.Connection do @group_prefix "rig::session::" @doc "Associates a connection process with a session name." - @spec associate_session(pid :: pid(), session_name :: String.t()) :: :ok - def associate_session(pid, session_name) when is_pid(pid) and byte_size(session_name) > 0 do - group = @group_prefix <> session_name + @spec associate_session(pid :: pid(), session_id :: String.t()) :: :ok + def associate_session(pid, session_id) when is_pid(pid) and byte_size(session_id) > 0 do + group = @group_prefix <> session_id # Ensure the session (group) exists: :ok = :pg2.create(group) @@ -23,16 +23,16 @@ defmodule RIG.Session.Connection do end @doc "Tells all connection processes associated with a session name to terminate." - @spec terminate_all_associated_to(session_name :: String.t()) :: :ok - def terminate_all_associated_to(session_name) when byte_size(session_name) > 0 do - group = @group_prefix <> session_name + @spec terminate_all_associated_to(session_id :: String.t()) :: :ok + def terminate_all_associated_to(session_id) when byte_size(session_id) > 0 do + group = @group_prefix <> session_id case :pg2.get_members(group) do {:error, {:no_such_group, ^group}} -> :ok members -> - for pid <- members, do: send(pid, {:session_killed, session_name}) + for pid <- members, do: send(pid, {:session_killed, session_id}) :ok = :pg2.delete(group) end end diff --git a/lib/rig_api/controllers/session_blacklist_controller.ex b/lib/rig_api/controllers/session_blacklist_controller.ex index 394524ab..2116b0be 100644 --- a/lib/rig_api/controllers/session_blacklist_controller.ex +++ b/lib/rig_api/controllers/session_blacklist_controller.ex @@ -66,8 +66,8 @@ defmodule RigApi.SessionBlacklistController do the token's expiration timestamp. This has the following consequences: - Any existing connection related to the session is terminated immediately. - - The related authorization token is ignored when a client establishes a connection. - - The related authorization token is ignored when a client creates a subscription. + - The related authorization token is no longer valid when a client establishes a connection. + - The related authorization token is no longer valid when a client creates a subscription. """) parameters do @@ -79,7 +79,7 @@ defmodule RigApi.SessionBlacklistController do ) end - response(200, "Ok", Schema.ref(:SessionBlacklistResponse)) + response(201, "Ok", Schema.ref(:SessionBlacklistResponse)) response(400, "Missing value for 'x'") end @@ -94,31 +94,59 @@ defmodule RigApi.SessionBlacklistController do {:ok, %{session_id: session_id, ttl_s: ttl_s}} -> Session.blacklist(session_id, ttl_s) - json(conn, %{ - "sessionId" => session_id, - "validityInSeconds" => ttl_s, - "isBlacklisted" => true - }) + send_resp( + conn, + :created, + Jason.encode!(%{ + "sessionId" => session_id, + "validityInSeconds" => ttl_s, + "isBlacklisted" => true + }) + ) end end # --- defp parse(body) do - {:ok, - %{ - session_id: Map.fetch!(body, "sessionId"), - ttl_s: body |> Map.fetch!("validityInSeconds") |> String.to_integer() - }} - rescue - e in KeyError -> - {:error, "Missing value for '#{e.key}'"} - - e in ArgumentError -> - # This is likely String.to_integer/1, but we don't know for sure. - {:error, "Invalid request body: #{inspect(e)}"} + Result.ok(%{}) + |> Result.and_then(&parse_and_add_session_id(&1, body)) + |> Result.and_then(&parse_and_add_ttl_s(&1, body)) end + # --- + + defp parse_and_add_session_id(into, from) do + case Map.fetch(from, "sessionId") do + {:ok, value} when byte_size(value) > 0 -> {:ok, Map.merge(into, %{session_id: value})} + {:ok, value} -> {:error, "Expected non-empty string, got #{inspect(value)}"} + :error -> {:error, "Missing value for \"sessionId\""} + end + end + + # --- + + defp parse_and_add_ttl_s(into, from) do + case Map.fetch(from, "validityInSeconds") do + {:ok, value} when is_number(value) and value > 0 -> + {:ok, Map.merge(into, %{ttl_s: value})} + + {:ok, value} when byte_size(value) > 0 -> + case Integer.parse(value) do + {value, ""} -> parse_and_add_ttl_s(into, Map.put(from, "validityInSeconds", value)) + not_a_number -> {:error, "Expected a number, got #{inspect(not_a_number)}"} + end + + {:ok, value} -> + {:error, "Expected a number, got #{inspect(value)}"} + + :error -> + {:error, "Missing value for \"validityInSeconds\""} + end + end + + # --- + def swagger_definitions do %{ SessionBlacklistRequest: @@ -129,7 +157,7 @@ defmodule RigApi.SessionBlacklistController do sessionId(:string, "JWT ID (jti) claim", required: true) validityInSeconds( - :string, + :number, "Defines how long the JWT ID should be considered invalid. Typically set to the token's remaining life time.", required: true ) @@ -137,7 +165,7 @@ defmodule RigApi.SessionBlacklistController do example(%{ sessionId: "SomeSessionID123", - validityInSeconds: "60" + validityInSeconds: 60 }) end, SessionBlacklistResponse: @@ -147,7 +175,7 @@ defmodule RigApi.SessionBlacklistController do properties do sessionId(:string, "JWT ID (jti) claim", required: true) - validityInSeconds(:string, "Seconds how long a session should be blacklisted", + validityInSeconds(:number, "Seconds how long a session should be blacklisted", required: true ) @@ -156,7 +184,7 @@ defmodule RigApi.SessionBlacklistController do example(%{ sessionId: "SomeSessionID123", - validityInSeconds: "60", + validityInSeconds: 60, isBlacklisted: true }) end, diff --git a/lib/rig_inbound_gateway_web/connection_init.ex b/lib/rig_inbound_gateway_web/connection_init.ex index b9429c91..17f431fa 100644 --- a/lib/rig_inbound_gateway_web/connection_init.ex +++ b/lib/rig_inbound_gateway_web/connection_init.ex @@ -70,11 +70,19 @@ defmodule RigInboundGatewayWeb.ConnectionInit do on_success.(subscriptions) else {:error, %Subscriptions.Error{} = e} -> + Logger.warn(fn -> + pid = inspect(self()) + msg = Exception.message(e) + "Cannot accept #{conn_type} connection #{pid}: #{msg}" + end) + on_error.(Exception.message(e)) {:error, :not_authorized} -> - Logger.debug(fn -> - "Not authorized #{conn_type} with pid=#{inspect(self())}" + Logger.warn(fn -> + pid = inspect(self()) + msg = "not authorized" + "Cannot accept #{conn_type} connection #{pid}: #{msg}" end) on_error.("Subscription denied (not authorized).") diff --git a/lib/rig_inbound_gateway_web/v1/sse.ex b/lib/rig_inbound_gateway_web/v1/sse.ex index 201dc02d..1f963a1a 100644 --- a/lib/rig_inbound_gateway_web/v1/sse.ex +++ b/lib/rig_inbound_gateway_web/v1/sse.ex @@ -75,7 +75,7 @@ defmodule RigInboundGatewayWeb.V1.SSE do # Say hello to the client: Events.welcome_event() |> to_server_sent_event() - |> :cowboy_req.stream_events(:nofin, req) + |> send_via(req) # Enter the loop and wait for cloud events to forward to the client: state = %{subscriptions: subscriptions} @@ -104,7 +104,7 @@ defmodule RigInboundGatewayWeb.V1.SSE do # We send a heartbeat now: :heartbeat |> to_server_sent_event() - |> :cowboy_req.stream_events(:nofin, req) + |> send_via(req) # And schedule the next one: Process.send_after(self(), :heartbeat, @heartbeat_interval_ms) @@ -119,7 +119,7 @@ defmodule RigInboundGatewayWeb.V1.SSE do # Forward the event to the client: event |> to_server_sent_event() - |> :cowboy_req.stream_events(:nofin, req) + |> send_via(req) {:ok, req, state, :hibernate} end @@ -137,7 +137,7 @@ defmodule RigInboundGatewayWeb.V1.SSE do # Notify the client: Events.subscriptions_set(subscriptions) |> to_server_sent_event() - |> :cowboy_req.stream_events(:nofin, req) + |> send_via(req) {:ok, req, state, :hibernate} end @@ -150,13 +150,13 @@ defmodule RigInboundGatewayWeb.V1.SSE do end @impl :cowboy_loop - def info({:session_killed, group}, req, state) do - Logger.info("session killed: #{inspect(group)}") + def info({:session_killed, session_id}, req, state) do + Logger.info("Session killed: #{inspect(session_id)} - terminating SSE/#{inspect(self())}..") # We tell the client: :session_killed |> to_server_sent_event() - |> :cowboy_req.stream_events(:nofin, req) + |> send_via(req) # And close the connection: {:stop, req, state} @@ -164,12 +164,43 @@ defmodule RigInboundGatewayWeb.V1.SSE do # --- + @impl :cowboy_loop + def terminate(reason, _req, _state) do + Logger.debug(fn -> + pid = inspect(self()) + reason = "reason=" <> inspect(reason) + "Closing SSE connection (#{pid}, #{reason})" + end) + + :ok + end + + # --- + defp to_server_sent_event(:heartbeat), do: %{comment: "heartbeat"} - defp to_server_sent_event(:session_killed), do: %{comment: "Session killed."} + + defp to_server_sent_event(:session_killed) do + %{ + specversion: "0.2", + type: "rig.session_killed", + source: "rig", + id: UUID.uuid4(), + time: Timex.now() |> Timex.format!("{RFC3339}") + } + |> CloudEvent.parse!() + |> to_server_sent_event() + end defp to_server_sent_event(%CloudEvent{} = event), do: %{ data: event.json, event: CloudEvent.type!(event) } + + # --- + + defp send_via(event, cowboy_req) do + :cowboy_req.stream_events(event, :nofin, cowboy_req) + Logger.debug(fn -> "Sent via SSE: " <> inspect(event) end) + end end diff --git a/lib/rig_inbound_gateway_web/v1/websocket.ex b/lib/rig_inbound_gateway_web/v1/websocket.ex index 69c020c3..5bd7a290 100644 --- a/lib/rig_inbound_gateway_web/v1/websocket.ex +++ b/lib/rig_inbound_gateway_web/v1/websocket.ex @@ -98,10 +98,14 @@ defmodule RigInboundGatewayWeb.V1.Websocket do @doc ~S"The client may send this as the response to the :ping heartbeat." @impl :cowboy_websocket - def websocket_handle({:pong, _}, state), do: {:ok, state, :hibernate} + def websocket_handle({:pong, _app_data}, state), do: {:ok, state, :hibernate} @impl :cowboy_websocket def websocket_handle(:pong, state), do: {:ok, state, :hibernate} + @doc ~S"Allow the client to send :ping messages to test connectivity." + @impl :cowboy_websocket + def websocket_handle({:ping, app_data}, state), do: {:reply, {:pong, app_data}, :hibernate} + @impl :cowboy_websocket def websocket_handle(in_frame, state) do Logger.debug(fn -> "Unexpected WebSocket input: #{inspect(in_frame)}" end) @@ -146,14 +150,27 @@ defmodule RigInboundGatewayWeb.V1.Websocket do end @impl :cowboy_websocket - def websocket_info({:session_killed, group}, state) do - Logger.info("session killed: #{inspect(group)}") + def websocket_info({:session_killed, session_id}, state) do + Logger.info("Session killed: #{inspect(session_id)} - terminating WS/#{inspect(self())}..") # This will close the connection: {:reply, closing_frame("Session killed."), state} end # --- + @impl :cowboy_websocket + def terminate(reason, _req, _state) do + Logger.debug(fn -> + pid = inspect(self()) + reason = "reason=" <> inspect(reason) + "Closing WebSocket connection (#{pid}, #{reason})" + end) + + :ok + end + + # --- + defp frame(%CloudEvent{json: json}) do {:text, json} end diff --git a/test/blacklist_test.exs b/test/blacklist_test.exs new file mode 100644 index 00000000..5837f25b --- /dev/null +++ b/test/blacklist_test.exs @@ -0,0 +1,107 @@ +defmodule BlacklistTest do + @moduledoc """ + Blacklisting a session should terminate active connections and prevent new ones. + """ + use ExUnit.Case, async: true + + alias RIG.JWT + + @rig_api_url "http://localhost:4010/" + + describe "After blacklisting a session," do + test "the API reports the session to be blacklisted." do + session_id = "some random string 90238490829084902342" + blacklist(session_id) + assert blacklisted?(session_id) + end + + test "new connections using the same session are no longer allowed." do + # blacklist a JWT + session_id = "some random string 98908462643632748511213123" + blacklist(session_id) + + # try to connect and verify it doesn't work + jwt = new_jwt(%{"jti" => session_id}) + assert {:error, %{code: 400}} = SseClient.try_connect_then_disconnect(jwt: jwt) + assert {:error, _} = WsClient.try_connect_then_disconnect(jwt: jwt) + end + + test "active connections related to that session are terminated." do + # Connect to RIG using a JWT: + + session_id = "some random string 8902731973190231212" + jwt = new_jwt(%{"jti" => session_id}) + + assert {:ok, sse} = SseClient.connect(jwt: jwt) + {_, sse} = SseClient.read_welcome_event(sse) + {_, sse} = SseClient.read_subscriptions_set_event(sse) + + assert {:ok, ws} = WsClient.connect(jwt: jwt) + {_, ws} = WsClient.read_welcome_event(ws) + {_, ws} = WsClient.read_subscriptions_set_event(ws) + + # Create an additional connection using a different JWT: + + other_session_id = "some random string 97123689684290890423312" + other_jwt = new_jwt(%{"jti" => other_session_id}) + + assert {:ok, other_sse} = SseClient.connect(jwt: other_jwt) + {_, other_sse} = SseClient.read_welcome_event(other_sse) + {_, other_sse} = SseClient.read_subscriptions_set_event(other_sse) + + # Blacklist only the first JWT using RIG's HTTP API: + + blacklist(session_id) + + # Verify all connections but the last one have been dropped: + + assert {_event, sse} = SseClient.read_event(sse, "rig.session_killed") + assert {:closed, sse} = SseClient.status(sse) + + assert {:closed, ws} = WsClient.status(ws) + + assert {:ok, other_sse} = SseClient.refute_receive(other_sse) + assert {:open, other_sse} = SseClient.status(other_sse) + end + end + + test "In RIG 2.x, the session API supports passing validityInSeconds as a string." do + body = + %{validityInSeconds: "123", sessionId: "some session name"} + |> Jason.encode!() + + {:ok, %HTTPoison.Response{status_code: 201}} = + HTTPoison.post("#{@rig_api_url}/v1/session-blacklist", body, [ + {"content-type", "application/json"} + ]) + end + + # --- + + defp blacklist(session_id) do + body = + %{validityInSeconds: 60, sessionId: session_id} + |> Jason.encode!() + + {:ok, %HTTPoison.Response{status_code: 201}} = + HTTPoison.post("#{@rig_api_url}/v1/session-blacklist", body, [ + {"content-type", "application/json"} + ]) + end + + # --- + + defp blacklisted?(jti) do + {:ok, %HTTPoison.Response{status_code: 200, body: body}} = + HTTPoison.get("#{@rig_api_url}/v1/session-blacklist/#{URI.encode(jti)}") + + %{"isBlacklisted" => blacklisted?} = Jason.decode!(body) + blacklisted? + end + + # --- + + defp new_jwt(claims) do + JWT.encode(claims) + end +end diff --git a/test/connection_test.exs b/test/connection_test.exs index 7bf22913..a7c3d558 100644 --- a/test/connection_test.exs +++ b/test/connection_test.exs @@ -21,77 +21,36 @@ defmodule RigInboundGateway.ConnectionTest do :ok end - defp flush_mailbox do - receive do - _ -> flush_mailbox() - after - 10 -> :ok - end + defp try_sse(params \\ []) do + SseClient.try_connect_then_disconnect(params) end - defp try_sse(params) do - url = "http://localhost:#{@port}/_rig/v1/connection/sse?#{URI.encode_query(params)}" - - %HTTPoison.AsyncResponse{id: client} = - HTTPoison.get!(url, %{}, - stream_to: self(), - recv_timeout: 20_000 - ) - - status_code = - receive do - %HTTPoison.AsyncStatus{code: code} -> code - after - 2_000 -> raise "No response" - end - - flush_mailbox() - {:ok, ^client} = :hackney.stop_async(client) - flush_mailbox() - status_code + defp try_ws(params \\ []) do + WsClient.try_connect_then_disconnect(params) end defp try_longpolling(params) do url = "http://localhost:#{@port}/_rig/v1/connection/longpolling?#{URI.encode_query(params)}" - - %HTTPoison.Response{status_code: res_status} = HTTPoison.get!(url) - - res_status - end - - defp try_ws(params) do - {:ok, client} = - WebSocket.connect("localhost", @port, %{ - path: "/_rig/v1/connection/ws?#{URI.encode_query(params)}", - protocol: ["ws"] - }) - - result = - case WebSocket.recv!(client) do - {:text, payload} -> {:ok, payload} - {:close, :normal, reason} -> {:error, reason} - end - - :ok = WebSocket.close(client) - result + %HTTPoison.Response{status_code: status_code} = HTTPoison.get!(url) + status_code end describe "Parameter handling:" do test ~S(Neither "jwt" nor "subscriptions" are required to connect.") do - assert 200 = try_sse(jwt: nil, subscriptions: nil) - assert {:ok, _} = try_ws(jwt: nil, subscriptions: nil) + assert {:ok, _} = try_sse() + assert {:ok, _} = try_ws() assert 200 == try_longpolling(jwt: nil, subscriptions: nil) end test "Passing an invalid JWT closes the connection with a request error." do - assert 400 = try_sse(jwt: "foobar", subscriptions: nil) - assert {:error, _} = try_ws(jwt: "foobar", subscriptions: nil) + assert {:error, %{code: 400}} = try_sse(jwt: "foobar") + assert {:error, _} = try_ws(jwt: "foobar") assert 400 == try_longpolling(jwt: "foobar", subscriptions: nil) end test "Passing an invalid subscriptions value closes the connection with a request error." do - assert 400 = try_sse(jwt: nil, subscriptions: "can't { be [ parsed.") - assert {:error, _} = try_ws(jwt: nil, subscriptions: "can't { be [ parsed.") + assert {:error, %{code: 400}} = try_sse(subscriptions: "can't { be [ parsed.") + assert {:error, _} = try_ws(subscriptions: "can't { be [ parsed.") assert 400 == try_longpolling(jwt: nil, subscriptions: "can't { be [ parsed.") end end diff --git a/test/support/clients.ex b/test/support/clients.ex index ca3842ed..96fa2326 100644 --- a/test/support/clients.ex +++ b/test/support/clients.ex @@ -1,14 +1,3 @@ -defmodule Client do - @moduledoc false - @type client :: pid | reference | atom - @callback connect(params :: list) :: {:ok, pid} - @callback disconnect(client) :: :ok - @callback refute_receive(client) :: :ok - @callback read_event(client, event_type :: String.t()) :: map() - @callback read_welcome_event(client) :: map() - @callback read_subscriptions_set_event(client) :: map() -end - defmodule TestClient.ConnectionError do defexception [:code, :reason] @@ -20,6 +9,20 @@ defmodule TestClient.ConnectionError do "could not establish connection, server responded with #{inspect(code)}: #{inspect(reason)}" end +defmodule Client do + @moduledoc false + @type client :: pid | reference | atom | map + @callback connect(params :: list) :: {:ok, pid} + @callback disconnect(client) :: :ok + @callback status(client) :: {:open, client} | {:closed, client} + @callback refute_receive(client) :: :ok + @callback read_event(client, event_type :: String.t()) :: map() + @callback read_welcome_event(client) :: map() + @callback read_subscriptions_set_event(client) :: map() + @callback try_connect_then_disconnect(params :: list) :: + {:ok, status :: any} | {:error, reason :: %TestClient.ConnectionError{}} +end + defmodule SseClient do @moduledoc false @behaviour Client @@ -70,27 +73,39 @@ defmodule SseClient do 500 -> raise "No response" end - {:ok, client} + {:ok, %{client: client, status: :open}} end @impl true - def disconnect(client) do + def disconnect(%{client: client, status: :open}) do {:ok, ^client} = :hackney.stop_async(client) :ok end @impl true - def refute_receive(ignored_client_ref) do + def status(state) + def status(%{status: :closed} = state), do: {:closed, state} + + def status(%{client: client} = state) do + receive do + %HTTPoison.AsyncEnd{id: ^client} -> {:closed, %{state | status: :closed}} + after + 100 -> {:open, state} + end + end + + @impl true + def refute_receive(%{status: :open} = state) do receive do %HTTPoison.AsyncChunk{} = async_chunk -> raise "Unexpectedly received: #{inspect(async_chunk)}" after - 100 -> {:ok, ignored_client_ref} + 100 -> {:ok, state} end end @impl true - def read_event(ignored_client_ref, event_type) do + def read_event(%{status: :open} = state, event_type) do cloud_event = read_sse_chunk() |> extract_cloud_event() @@ -101,14 +116,26 @@ defmodule SseClient do %{"cloudEventsVersion" => "0.1", "eventType" => ^event_type} -> cloud_event end - {cloud_event, ignored_client_ref} + {cloud_event, state} end @impl true - def read_welcome_event(client), do: read_event(client, "rig.connection.create") + def read_welcome_event(state), do: read_event(state, "rig.connection.create") + + @impl true + def read_subscriptions_set_event(state), do: read_event(state, "rig.subscriptions_set") @impl true - def read_subscriptions_set_event(client), do: read_event(client, "rig.subscriptions_set") + def try_connect_then_disconnect(params \\ []) do + {:ok, client} = connect(params) + flush_mailbox() + disconnect(client) + {:ok, client} + rescue + err in [TestClient.ConnectionError] -> {:error, err} + after + flush_mailbox() + end defp read_sse_chunk do receive do @@ -186,25 +213,50 @@ defmodule WsClient do message end - {:ok, %{client: client, first_message: first_message}} + {:ok, %{client: client, first_message: first_message, status: :open}} end @impl true - def disconnect(%{client: client}) do + def disconnect(%{client: client, status: :open}) do :ok = WebSocket.close(client) end @impl true - def refute_receive(%{client: client, first_message: first_message}) do + def status(state) + def status(%{status: :closed} = state), do: {:closed, state} + + def status(%{client: client} = state) do + # No idea why, but the first ping is always successful, regardless of whether the + # connection is still alive. As a workaround, we simply invoke ping twice. + case WebSocket.ping(client) do + {:error, _} -> + {:closed, %{state | status: :closed}} + + _app_data -> + # The second ping is successful too when done right after the first one.. O_o + :timer.sleep(100) + + case WebSocket.ping(client) do + {:error, _} -> {:closed, %{state | status: :closed}} + _app_data -> {:open, state} + end + end + end + + @impl true + def refute_receive(%{client: client, first_message: first_message, status: :open} = state) do case first_message || WebSocket.recv(client, timeout: 100) do {:ping, _} -> client.refute_receive(client) {:ok, packet} -> raise "Unexpectedly received: #{inspect(packet)}" - {:error, _} -> {:ok, %{client: client, first_message: nil}} + {:error, _} -> {:ok, %{state | first_message: nil}} end end @impl true - def read_event(%{client: client, first_message: first_message}, event_type) do + def read_event( + %{client: client, first_message: first_message, status: :open} = state, + event_type + ) do {:text, data} = first_message || WebSocket.recv!(client) cloud_event = @@ -215,12 +267,21 @@ defmodule WsClient do %{"cloudEventsVersion" => "0.1", "eventType" => ^event_type} = cloud_event -> cloud_event end - {cloud_event, %{client: client, first_message: nil}} + {cloud_event, %{state | first_message: nil}} end @impl true - def read_welcome_event(client), do: read_event(client, "rig.connection.create") + def read_welcome_event(state), do: read_event(state, "rig.connection.create") + + @impl true + def read_subscriptions_set_event(state), do: read_event(state, "rig.subscriptions_set") @impl true - def read_subscriptions_set_event(client), do: read_event(client, "rig.subscriptions_set") + def try_connect_then_disconnect(params \\ []) do + {:ok, client} = connect(params) + disconnect(client) + {:ok, client} + rescue + err in [TestClient.ConnectionError] -> {:error, err} + end end From f9885a34f62c0cd92f1363fdef0e0b4994332be2 Mon Sep 17 00:00:00 2001 From: Kevin Bader Date: Tue, 10 Sep 2019 09:22:45 +0200 Subject: [PATCH 2/7] Add RIG_API v2 so as to introduce breaking changes to session-blacklist --- CHANGELOG.md | 12 +- config/rig_api/config.exs | 3 +- config/rig_api/test.exs | 3 +- .../frontend/package-lock.json | 82 +++-- .../fallback_controller.ex => fallback.ex} | 2 +- .../health_controller.ex => health.ex} | 2 +- lib/rig_api/router.ex | 76 +++-- lib/rig_api/v1/REMOVE_WITH_3.0_RELEASE | 1 + .../apis_controller.ex => v1/apis.ex} | 14 +- .../message_controller.ex => v1/messages.ex} | 8 +- .../responses.ex} | 8 +- .../session_blacklist.ex} | 40 ++- lib/rig_api/v2/SINCE_2.3 | 0 lib/rig_api/v2/apis.ex | 289 ++++++++++++++++++ lib/rig_api/v2/messages.ex | 90 ++++++ lib/rig_api/v2/responses.ex | 117 +++++++ lib/rig_api/v2/session_blacklist.ex | 191 ++++++++++++ test/blacklist_test.exs | 13 +- test/connection_test.exs | 1 - .../health_controller_test.exs | 0 .../apis_controller_test.exs | 2 +- .../message_controller_test.exs | 2 +- test/support/api_proxy_injection.ex | 17 +- 23 files changed, 866 insertions(+), 107 deletions(-) rename lib/rig_api/{controllers/fallback_controller.ex => fallback.ex} (89%) rename lib/rig_api/{controllers/health_controller.ex => health.ex} (88%) create mode 100644 lib/rig_api/v1/REMOVE_WITH_3.0_RELEASE rename lib/rig_api/{controllers/apis_controller.ex => v1/apis.ex} (97%) rename lib/rig_api/{controllers/message_controller.ex => v1/messages.ex} (95%) rename lib/rig_api/{controllers/responses_controller.ex => v1/responses.ex} (96%) rename lib/rig_api/{controllers/session_blacklist_controller.ex => v1/session_blacklist.ex} (84%) create mode 100644 lib/rig_api/v2/SINCE_2.3 create mode 100644 lib/rig_api/v2/apis.ex create mode 100644 lib/rig_api/v2/messages.ex create mode 100644 lib/rig_api/v2/responses.ex create mode 100644 lib/rig_api/v2/session_blacklist.ex rename test/rig_api/{controllers => }/health_controller_test.exs (100%) rename test/rig_api/{controllers => v1}/apis_controller_test.exs (98%) rename test/rig_api/{controllers => v1}/message_controller_test.exs (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95efea02..0f84b028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added longpolling as new connection type [#217](https://github.com/Accenture/reactive-interaction-gateway/issues/217) - When terminating an SSE connection after its associated session has been blacklisted, RIG now sends out a `rig.session_killed` event before closing the socket. +- New API for querying and updating the session blacklist: `/v2/session-blacklist`, which introduces the following breaking changes: + - When a session has been added to the session blacklist successfully, the endpoint now uses the correct HTTP status code "201 Created" instead of "200 Ok". + - When using the API to blacklist a session, the `validityInSeconds` should now be passed as an integer value (using a string still works though). -### Changed - -- When a session has been added to the session blacklist successfully, the endpoint now uses the correct HTTP status code "201 Created" instead of "200 Ok". -- When using the API to blacklist a session, the `validityInSeconds` should now be passed as an integer value (see `Deprecated` below). + ### Fixed @@ -23,9 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Logging incoming HTTP request to Kafka works again and now also supports Apache Avro. [#170](https://github.com/Accenture/reactive-interaction-gateway/issues/170) -### Deprecated - -- When using the API to blacklist a session, passing the `validityInSeconds` field as a string is deprecated (but supported until the 3.0 release). Please use an integer instead. + ### Removed diff --git a/config/rig_api/config.exs b/config/rig_api/config.exs index 8b54b9ed..9aa0d5a6 100644 --- a/config/rig_api/config.exs +++ b/config/rig_api/config.exs @@ -43,7 +43,8 @@ config :rig, RigApi.Endpoint, # Always start the HTTP endpoints on application startup: config :phoenix, :serve_endpoints, true -config :rig, RigApi.ApisController, rig_proxy: RigInboundGateway.Proxy +config :rig, RigApi.V1.APIs, rig_proxy: RigInboundGateway.Proxy +config :rig, RigApi.V2.APIs, rig_proxy: RigInboundGateway.Proxy config :rig, :event_filter, Rig.EventFilter diff --git a/config/rig_api/test.exs b/config/rig_api/test.exs index 8856d69a..1287a86a 100644 --- a/config/rig_api/test.exs +++ b/config/rig_api/test.exs @@ -8,6 +8,7 @@ config :rig, RigApi.Endpoint, password: "test" ] -config :rig, RigApi.ApisController, rig_proxy: RigInboundGateway.ProxyMock +config :rig, RigApi.V1.APIs, rig_proxy: RigInboundGateway.ProxyMock +config :rig, RigApi.V2.APIs, rig_proxy: RigInboundGateway.ProxyMock config :rig, :event_filter, Rig.EventFilterMock diff --git a/examples/channels-example/frontend/package-lock.json b/examples/channels-example/frontend/package-lock.json index e72676a1..301be3f4 100644 --- a/examples/channels-example/frontend/package-lock.json +++ b/examples/channels-example/frontend/package-lock.json @@ -3157,7 +3157,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -3178,12 +3179,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3198,17 +3201,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3325,7 +3331,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3337,6 +3344,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3351,6 +3359,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3358,12 +3367,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3382,6 +3393,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3462,7 +3474,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3474,6 +3487,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3559,7 +3573,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3595,6 +3610,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3614,6 +3630,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3657,12 +3674,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -7521,7 +7540,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -7542,12 +7562,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7562,17 +7584,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -7689,7 +7714,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -7701,6 +7727,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7715,6 +7742,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7722,12 +7750,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7746,6 +7776,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -7826,7 +7857,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -7838,6 +7870,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -7923,7 +7956,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -7959,6 +7993,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7978,6 +8013,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -8021,12 +8057,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } } diff --git a/lib/rig_api/controllers/fallback_controller.ex b/lib/rig_api/fallback.ex similarity index 89% rename from lib/rig_api/controllers/fallback_controller.ex rename to lib/rig_api/fallback.ex index 671142dd..6d4e6013 100644 --- a/lib/rig_api/controllers/fallback_controller.ex +++ b/lib/rig_api/fallback.ex @@ -1,4 +1,4 @@ -defmodule RigApi.FallbackController do +defmodule RigApi.Fallback do @moduledoc """ Translates controller action results into valid `Plug.Conn` responses. diff --git a/lib/rig_api/controllers/health_controller.ex b/lib/rig_api/health.ex similarity index 88% rename from lib/rig_api/controllers/health_controller.ex rename to lib/rig_api/health.ex index f17cfa40..2f3b7d3b 100644 --- a/lib/rig_api/controllers/health_controller.ex +++ b/lib/rig_api/health.ex @@ -1,4 +1,4 @@ -defmodule RigApi.HealthController do +defmodule RigApi.Health do require Logger use RigApi, :controller diff --git a/lib/rig_api/router.ex b/lib/rig_api/router.ex index b44d7eb7..f5291eac 100644 --- a/lib/rig_api/router.ex +++ b/lib/rig_api/router.ex @@ -1,7 +1,7 @@ defmodule RigApi.Router do use RigApi, :router - pipeline :api do + pipeline :body_parser do plug(Plug.Parsers, parsers: [:urlencoded, :multipart, :json], # return "415 Unsupported Media Type" if not handled by any parser @@ -10,39 +10,69 @@ defmodule RigApi.Router do ) end - scope "/v1/messages", RigApi do - post("/", MessageController, :publish) + scope "/health", RigApi do + pipe_through(:body_parser) + get("/", Health, :check_health) + end + + scope "/swagger-ui" do + forward("/", PhoenixSwagger.Plug.SwaggerUI, + otp_app: :rig, + swagger_file: "rig_api_swagger.json" + ) end - scope "/v1", RigApi do - pipe_through(:api) + # Deprecated in 2.3, to be removed with 3.0: + scope "/v1", RigApi.V1 do + scope "/apis" do + pipe_through(:body_parser) + get("/", APIs, :list_apis) + post("/", APIs, :add_api) + get("/:id", APIs, :get_api_detail) + put("/:id", APIs, :update_api) + delete("/:id", APIs, :deactivate_api) + end - resources("/responses", ResponsesController, only: [:create]) + scope "/messages" do + post("/", Messages, :publish) + end + + scope "/responses" do + pipe_through(:body_parser) + resources("/", Responses, only: [:create]) + end scope "/session-blacklist" do - post("/", SessionBlacklistController, :blacklist_session) - get("/:session_id", SessionBlacklistController, :check_status) + pipe_through(:body_parser) + post("/", SessionBlacklist, :blacklist_session) + get("/:session_id", SessionBlacklist, :check_status) end + end + scope "/v2", RigApi.V2 do scope "/apis" do - get("/", ApisController, :list_apis) - post("/", ApisController, :add_api) - get("/:id", ApisController, :get_api_detail) - put("/:id", ApisController, :update_api) - delete("/:id", ApisController, :deactivate_api) + pipe_through(:body_parser) + get("/", APIs, :list_apis) + post("/", APIs, :add_api) + get("/:id", APIs, :get_api_detail) + put("/:id", APIs, :update_api) + delete("/:id", APIs, :deactivate_api) end - end - scope "/health", RigApi do - pipe_through(:api) - get("/", HealthController, :check_health) - end + scope "/messages" do + post("/", Messages, :publish) + end - scope "/swagger-ui" do - forward("/", PhoenixSwagger.Plug.SwaggerUI, - otp_app: :rig, - swagger_file: "rig_api_swagger.json" - ) + scope "/responses" do + pipe_through(:body_parser) + resources("/", Responses, only: [:create]) + end + + scope "/session-blacklist" do + pipe_through(:body_parser) + post("/", SessionBlacklist, :blacklist_session) + get("/:session_id", SessionBlacklist, :check_status) + end end def swagger_info do diff --git a/lib/rig_api/v1/REMOVE_WITH_3.0_RELEASE b/lib/rig_api/v1/REMOVE_WITH_3.0_RELEASE new file mode 100644 index 00000000..53f29297 --- /dev/null +++ b/lib/rig_api/v1/REMOVE_WITH_3.0_RELEASE @@ -0,0 +1 @@ +/v1 is deprecated in 2.3 and should be removed with 3.0 diff --git a/lib/rig_api/controllers/apis_controller.ex b/lib/rig_api/v1/apis.ex similarity index 97% rename from lib/rig_api/controllers/apis_controller.ex rename to lib/rig_api/v1/apis.ex index 6fddd475..59645d21 100644 --- a/lib/rig_api/controllers/apis_controller.ex +++ b/lib/rig_api/v1/apis.ex @@ -1,4 +1,4 @@ -defmodule RigApi.ApisController do +defmodule RigApi.V1.APIs do @moduledoc """ HTTP-accessible API for managing PROXY APIs. @@ -8,8 +8,10 @@ defmodule RigApi.ApisController do use PhoenixSwagger require Logger + @prefix "/v1" + swagger_path :list_apis do - get("/v1/apis") + get(@prefix <> "/apis") summary("List current proxy API-definitions.") response(200, "Ok", Schema.ref(:ProxyAPIList)) end @@ -22,7 +24,7 @@ defmodule RigApi.ApisController do end swagger_path :get_api_detail do - get("/v1/apis/{apiId}") + get(@prefix <> "/apis/{apiId}") summary("Obtain details on a proxy API-definition.") parameters do @@ -46,7 +48,7 @@ defmodule RigApi.ApisController do end swagger_path :add_api do - post("/v1/apis") + post(@prefix <> "/apis") summary("Register a new proxy API-definition.") parameters do @@ -80,7 +82,7 @@ defmodule RigApi.ApisController do end swagger_path :update_api do - put("/v1/apis/{apiId}") + put(@prefix <> "/apis/{apiId}") summary("Update a proxy API-definition.") parameters do @@ -111,7 +113,7 @@ defmodule RigApi.ApisController do end swagger_path :deactivate_api do - delete("/v1/apis/{apiId}") + delete(@prefix <> "/apis/{apiId}") summary("Deactivate a proxy API-definition.") parameters do diff --git a/lib/rig_api/controllers/message_controller.ex b/lib/rig_api/v1/messages.ex similarity index 95% rename from lib/rig_api/controllers/message_controller.ex rename to lib/rig_api/v1/messages.ex index 5b7cd875..a30c90c9 100644 --- a/lib/rig_api/controllers/message_controller.ex +++ b/lib/rig_api/v1/messages.ex @@ -1,4 +1,4 @@ -defmodule RigApi.MessageController do +defmodule RigApi.V1.Messages do require Logger use RigApi, :controller @@ -6,10 +6,12 @@ defmodule RigApi.MessageController do alias RIG.Sources.HTTP.Handler - action_fallback(RigApi.FallbackController) + @prefix "/v1" + + action_fallback(RigApi.Fallback) swagger_path :publish do - post("/v1/messages") + post(@prefix <> "/messages") summary("Submit an event, to be forwarded to subscribed frontends.") description("Allows you to submit a single event to RIG using a simple, \ synchronous call. While for production setups we recommend ingesting events \ diff --git a/lib/rig_api/controllers/responses_controller.ex b/lib/rig_api/v1/responses.ex similarity index 96% rename from lib/rig_api/controllers/responses_controller.ex rename to lib/rig_api/v1/responses.ex index f6cd0ceb..8303d58b 100644 --- a/lib/rig_api/controllers/responses_controller.ex +++ b/lib/rig_api/v1/responses.ex @@ -1,4 +1,4 @@ -defmodule RigApi.ResponsesController do +defmodule RigApi.V1.Responses do require Logger use RigApi, :controller @@ -7,10 +7,12 @@ defmodule RigApi.ResponsesController do alias Rig.Connection.Codec alias RigCloudEvents.CloudEvent - action_fallback(RigApi.FallbackController) + @prefix "/v1" + + action_fallback(RigApi.Fallback) swagger_path :create do - post("/v1/responses") + post(@prefix <> "/responses") summary("Submit a message, to be sent to correlated reverse proxy request.") description("Allows you to submit a message to RIG using a simple, \ synchronous call. Message will be sent to correlated reverse proxy request.") diff --git a/lib/rig_api/controllers/session_blacklist_controller.ex b/lib/rig_api/v1/session_blacklist.ex similarity index 84% rename from lib/rig_api/controllers/session_blacklist_controller.ex rename to lib/rig_api/v1/session_blacklist.ex index 2116b0be..7114ae50 100644 --- a/lib/rig_api/controllers/session_blacklist_controller.ex +++ b/lib/rig_api/v1/session_blacklist.ex @@ -1,4 +1,4 @@ -defmodule RigApi.SessionBlacklistController do +defmodule RigApi.V1.SessionBlacklist do @moduledoc """ Allows for blocking "sessions" for a specific period of time. @@ -13,6 +13,7 @@ defmodule RigApi.SessionBlacklistController do alias RIG.Session + @prefix "/v1" @cors_origins "*" # --- @@ -35,7 +36,7 @@ defmodule RigApi.SessionBlacklistController do # --- swagger_path :check_status do - get("/v1/session-blacklist/{sessionId}") + get(@prefix <> "/session-blacklist/{sessionId}") summary("Check whether a given session is currently blacklisted.") parameters do @@ -58,7 +59,7 @@ defmodule RigApi.SessionBlacklistController do # --- swagger_path :blacklist_session do - post("/v1/session-blacklist") + post(@prefix <> "/session-blacklist") summary("Add a session to the session blacklist.") description(""" @@ -79,7 +80,7 @@ defmodule RigApi.SessionBlacklistController do ) end - response(201, "Ok", Schema.ref(:SessionBlacklistResponse)) + response(200, "Ok", Schema.ref(:SessionBlacklistResponse)) response(400, "Missing value for 'x'") end @@ -94,15 +95,11 @@ defmodule RigApi.SessionBlacklistController do {:ok, %{session_id: session_id, ttl_s: ttl_s}} -> Session.blacklist(session_id, ttl_s) - send_resp( - conn, - :created, - Jason.encode!(%{ - "sessionId" => session_id, - "validityInSeconds" => ttl_s, - "isBlacklisted" => true - }) - ) + json(conn, %{ + "sessionId" => session_id, + "validityInSeconds" => ttl_s, + "isBlacklisted" => true + }) end end @@ -128,17 +125,14 @@ defmodule RigApi.SessionBlacklistController do defp parse_and_add_ttl_s(into, from) do case Map.fetch(from, "validityInSeconds") do - {:ok, value} when is_number(value) and value > 0 -> - {:ok, Map.merge(into, %{ttl_s: value})} - {:ok, value} when byte_size(value) > 0 -> case Integer.parse(value) do - {value, ""} -> parse_and_add_ttl_s(into, Map.put(from, "validityInSeconds", value)) - not_a_number -> {:error, "Expected a number, got #{inspect(not_a_number)}"} + {value, ""} when value > 0 -> {:ok, Map.merge(into, %{ttl_s: value})} + _ -> {:error, "Expected a positive number in a string, got #{inspect(value)}"} end {:ok, value} -> - {:error, "Expected a number, got #{inspect(value)}"} + {:error, "Expected a positive number in a string, got #{inspect(value)}"} :error -> {:error, "Missing value for \"validityInSeconds\""} @@ -157,7 +151,7 @@ defmodule RigApi.SessionBlacklistController do sessionId(:string, "JWT ID (jti) claim", required: true) validityInSeconds( - :number, + :string, "Defines how long the JWT ID should be considered invalid. Typically set to the token's remaining life time.", required: true ) @@ -165,7 +159,7 @@ defmodule RigApi.SessionBlacklistController do example(%{ sessionId: "SomeSessionID123", - validityInSeconds: 60 + validityInSeconds: "60" }) end, SessionBlacklistResponse: @@ -175,7 +169,7 @@ defmodule RigApi.SessionBlacklistController do properties do sessionId(:string, "JWT ID (jti) claim", required: true) - validityInSeconds(:number, "Seconds how long a session should be blacklisted", + validityInSeconds(:string, "Seconds how long a session should be blacklisted", required: true ) @@ -184,7 +178,7 @@ defmodule RigApi.SessionBlacklistController do example(%{ sessionId: "SomeSessionID123", - validityInSeconds: 60, + validityInSeconds: "60", isBlacklisted: true }) end, diff --git a/lib/rig_api/v2/SINCE_2.3 b/lib/rig_api/v2/SINCE_2.3 new file mode 100644 index 00000000..e69de29b diff --git a/lib/rig_api/v2/apis.ex b/lib/rig_api/v2/apis.ex new file mode 100644 index 00000000..5e1a784b --- /dev/null +++ b/lib/rig_api/v2/apis.ex @@ -0,0 +1,289 @@ +defmodule RigApi.V2.APIs do + @moduledoc """ + HTTP-accessible API for managing PROXY APIs. + + """ + use Rig.Config, [:rig_proxy] + use RigApi, :controller + use PhoenixSwagger + require Logger + + @prefix "/v2" + + swagger_path :list_apis do + get(@prefix <> "/apis") + summary("List current proxy API-definitions.") + response(200, "Ok", Schema.ref(:ProxyAPIList)) + end + + def list_apis(conn, _params) do + %{rig_proxy: proxy} = config() + api_defs = proxy.list_apis(proxy) + active_apis = for {_, api} <- api_defs, api["active"], do: api + send_response(conn, 200, active_apis) + end + + swagger_path :get_api_detail do + get(@prefix <> "/apis/{apiId}") + summary("Obtain details on a proxy API-definition.") + + parameters do + apiId(:path, :string, "API definition identifier", required: true, example: "new-service") + end + + response(200, "Ok", Schema.ref(:ProxyAPI)) + response(404, "Doesn't exist", Schema.ref(:ProxyAPIResponse)) + end + + def get_api_detail(conn, params) do + %{"id" => id} = params + + case get_active_api(id) do + api when api == nil or api == :inactive -> + send_response(conn, 404, %{message: "API with id=#{id} doesn't exists."}) + + {_id, api} -> + send_response(conn, 200, api) + end + end + + swagger_path :add_api do + post(@prefix <> "/apis") + summary("Register a new proxy API-definition.") + + parameters do + proxyAPI( + :body, + Schema.ref(:ProxyAPI), + "The details for the new Proxy endpoint", + required: true + ) + end + + response(201, "Ok", Schema.ref(:ProxyAPIResponse)) + response(409, "Already exists", Schema.ref(:ProxyAPIResponse)) + end + + def add_api(conn, params) do + %{"id" => id} = params + %{rig_proxy: proxy} = config() + + with nil <- proxy.get_api(proxy, id), + {:ok, _phx_ref} <- proxy.add_api(proxy, id, params) do + send_response(conn, 201, %{message: "ok"}) + else + {_id, %{"active" => true}} -> + send_response(conn, 409, %{message: "API with id=#{id} already exists."}) + + {_id, %{"active" => false} = prev_api} -> + {:ok, _phx_ref} = proxy.replace_api(proxy, id, prev_api, params) + send_response(conn, 201, %{message: "ok"}) + end + end + + swagger_path :update_api do + put(@prefix <> "/apis/{apiId}") + summary("Update a proxy API-definition.") + + parameters do + apiId(:path, :string, "API definition identifier", required: true, example: "new-service") + + proxyAPI( + :body, + Schema.ref(:ProxyAPI), + "The details for the new Proxy endpoint", + required: true + ) + end + + response(200, "Ok", Schema.ref(:ProxyAPIResponse)) + response(404, "Doesn't exist", Schema.ref(:ProxyAPIResponse)) + end + + def update_api(conn, params) do + %{"id" => id} = params + + with {_id, current_api} <- get_active_api(id), + {:ok, _phx_ref} <- merge_and_update(id, current_api, params) do + send_response(conn, 200, %{message: "ok"}) + else + api when api == nil or api == :inactive -> + send_response(conn, 404, %{message: "API with id=#{id} doesn't exists."}) + end + end + + swagger_path :deactivate_api do + delete(@prefix <> "/apis/{apiId}") + summary("Deactivate a proxy API-definition.") + + parameters do + apiId(:path, :string, "API definition identifier", required: true, example: "new-service") + end + + response(204, "Deleted") + response(404, "Doesn't exist", Schema.ref(:ProxyAPIResponse)) + end + + def deactivate_api(conn, params) do + %{"id" => id} = params + %{rig_proxy: proxy} = config() + + with {_id, _current_api} <- get_active_api(id), + {:ok, _phx_ref} <- proxy.deactivate_api(proxy, id) do + send_response(conn, 204) + else + api when api == nil or api == :inactive -> + send_response(conn, 404, %{message: "API with id=#{id} doesn't exists."}) + end + end + + defp get_active_api(id) do + %{rig_proxy: proxy} = config() + + with {id, current_api} <- proxy.get_api(proxy, id), + true <- current_api["active"] == true do + {id, current_api} + else + nil -> nil + false -> :inactive + _ -> :error + end + end + + defp merge_and_update(id, current_api, updated_api) do + %{rig_proxy: proxy} = config() + merged_api = current_api |> Map.merge(updated_api) + proxy.update_api(proxy, id, merged_api) + end + + defp send_response(conn, status_code, body \\ %{}) do + conn + |> put_status(status_code) + |> json(body) + end + + def swagger_definitions do + %{ + ProxyAPI: + swagger_schema do + title("Proxy API Object") + description("An Proxy API object - Is used for creating/updating/reading") + + properties do + id(:string, "Proxy API ID", required: true, example: "new-service") + name(:string, "Proxy API Name", required: true, example: "new-service") + auth_type(:string, "Authorization type", required: true, example: "jwt") + + auth( + Schema.new do + properties do + use_header(:boolean, "Authorization Header Usage", default: true, example: true) + + header_name(:string, "Authorization Header Name", + required: true, + example: "Authorization" + ) + + use_query(:boolean, "Authorization Header Query Usage", + default: false, + example: false + ) + + query_name(:string, "Authorization Header Query Name", required: false) + end + end + ) + + timestamp(:string, "creation timestamp", + required: false, + example: "2018-12-17T10:38:06.334013Z" + ) + + ref_number(:integer, "reference number", required: false, example: 0) + node_name(:string, "Node name", required: false, example: "nonode@nohost") + active(:boolean, "ID Status", required: false, example: true) + phx_ref(:string, "Phoenix Reference", required: false, example: "ewTJVcM7Bzc=") + versioned(:boolean, "is Versioned Endpoint?", default: false, example: false) + + version_data( + Schema.new do + properties do + default( + Schema.new do + properties do + endpoints(Schema.ref(:ProxyAPIEndpointArray)) + end + end + ) + end + end + ) + + proxy( + Schema.new do + properties do + use_env(:boolean, "TBD", default: true, example: true) + target_url(:string, "Proxy Target URL", required: true, example: "IS_HOST") + port(:integer, "Proxy Port", required: true, example: 6666) + end + end + ) + end + end, + ProxyAPIEndpointArray: + swagger_schema do + title("Proxy API Endpoint Array") + description("Array of Endpoints for the Proxy API") + type(:array) + items(Schema.ref(:ProxyAPIEndpoint)) + end, + ProxyAPIEndpoint: + swagger_schema do + title("Proxy API Endpoint") + description("Endpoint for the Proxy API for Request") + + properties do + id(:string, "Endpoint ID", required: true, example: "get-auth-register") + + path(:string, "Endpoint path. Curly braces may be used to ignore parts of the path.", + required: false, + example: "/auth/register/{user}" + ) + + path_regex( + :string, + "Endpoint path, given as a regular expression (note that JSON requires escaping backslash characters).", + required: false, + example: "/auth/register/(.+)" + ) + + path_replacement( + :string, + "If given, the request path is rewritten. When used with `path_regex`, capture groups can be referenced by number (note that JSON requires escaping backslash characters).", + required: false, + example: ~S"/auth/register/\1" + ) + + method(:string, "Endpoint HTTP method", required: true, example: "GET") + secured(:boolean, "Endpoint Security", default: false, example: false) + end + end, + ProxyAPIResponse: + swagger_schema do + title("Proxy API Response") + description("Proxy API Response") + + properties do + message(:string, "Response", required: true, example: "ok") + end + end, + ProxyAPIList: + swagger_schema do + title("Proxy API List") + description(" A List of parameterized Proxy APIs") + type(:array) + items(Schema.ref(:ProxyAPI)) + end + } + end +end diff --git a/lib/rig_api/v2/messages.ex b/lib/rig_api/v2/messages.ex new file mode 100644 index 00000000..c91895d1 --- /dev/null +++ b/lib/rig_api/v2/messages.ex @@ -0,0 +1,90 @@ +defmodule RigApi.V2.Messages do + require Logger + + use RigApi, :controller + use PhoenixSwagger + + alias RIG.Sources.HTTP.Handler + + @prefix "/v2" + + action_fallback(RigApi.Fallback) + + swagger_path :publish do + post(@prefix <> "/messages") + summary("Submit an event, to be forwarded to subscribed frontends.") + description("Allows you to submit a single event to RIG using a simple, \ + synchronous call. While for production setups we recommend ingesting events \ + asynchronously (e.g., via a Kafka topic), using this endpoint can be simple \ + alternative during development or for low-traffic production setups.") + + parameters do + messageBody( + :body, + Schema.ref(:CloudEvent), + "CloudEvent", + required: true + ) + end + + response(202, "Accepted - message queued for transport") + response(400, "Bad Request: Failed to parse request body :parse-error") + end + + @doc """ + Accepts message to be sent to front-ends. + """ + def publish(%{method: "POST"} = conn, _params) do + Handler.handle_http_submission(conn, check_authorization?: false) + end + + # --- + + def swagger_definitions do + %{ + CloudEvent: + swagger_schema do + title("CloudEvent") + description("The broadcasted CloudEvent according to the CloudEvents spec.") + + properties do + id( + :string, + "ID of the event. The semantics of this string are explicitly undefined to ease \ + the implementation of producers. Enables deduplication.", + required: true, + example: "A database commit ID" + ) + + specversion( + :string, + "The version of the CloudEvents specification which the event uses. This \ + enables the interpretation of the context. Compliant event producers \ + MUST use a value of 0.2 when referring to this version of the \ + specification.", + required: true, + example: "0.2" + ) + + source( + :string, + "This describes the event producer. Often this will include information such \ + as the type of the event source, the organization publishing the event, the \ + process that produced the event, and some unique identifiers. The exact syntax \ + and semantics behind the data encoded in the URI is event producer defined.", + required: true, + example: "/cloudevents/spec/pull/123" + ) + + type( + :string, + "Type of occurrence which has happened. Often this attribute is used for \ + routing, observability, policy enforcement, etc.", + required: true, + example: "com.example.object.delete.v2" + ) + end + end + } + end +end diff --git a/lib/rig_api/v2/responses.ex b/lib/rig_api/v2/responses.ex new file mode 100644 index 00000000..4c1b0801 --- /dev/null +++ b/lib/rig_api/v2/responses.ex @@ -0,0 +1,117 @@ +defmodule RigApi.V2.Responses do + require Logger + + use RigApi, :controller + use PhoenixSwagger + + alias Rig.Connection.Codec + alias RigCloudEvents.CloudEvent + + @prefix "/v2" + + action_fallback(RigApi.Fallback) + + swagger_path :create do + post(@prefix <> "/responses") + summary("Submit a message, to be sent to correlated reverse proxy request.") + description("Allows you to submit a message to RIG using a simple, \ + synchronous call. Message will be sent to correlated reverse proxy request.") + + parameters do + messageBody( + :body, + Schema.ref(:CloudEvent), + "CloudEvent", + required: true + ) + end + + response(202, "Accepted - message sent to correlated reverse proxy request") + response(400, "Bad Request: Failed to parse request body :parse-error") + end + + @doc """ + Accepts message to be sent to correlated HTTP process. + + Note that body has to contain following field `"rig": { "correlation": "_id_" }`. + """ + def create(conn, message) do + with {:ok, cloud_event} <- CloudEvent.parse(message), + {:ok, rig_metadata} <- Map.fetch(message, "rig"), + {:ok, correlation_id} <- Map.fetch(rig_metadata, "correlation"), + {:ok, deserialized_pid} <- Codec.deserialize(correlation_id) do + Logger.debug(fn -> + "HTTP response via internal HTTP to #{inspect(deserialized_pid)}: #{inspect(message)}" + end) + + send(deserialized_pid, {:response_received, cloud_event.json}) + send_resp(conn, :accepted, "message sent to correlated reverse proxy request") + else + err -> + Logger.warn(fn -> "Parse error #{inspect(err)} for #{inspect(message)}" end) + + conn + |> put_status(:bad_request) + |> text("Failed to parse request body: #{inspect(err)}") + end + end + + def swagger_definitions do + %{ + Response: + swagger_schema do + title("CloudEvent") + description("The CloudEvent that will be sent to correlated reverse proxy request.") + + properties do + id( + :string, + "ID of the event. The semantics of this string are explicitly undefined to ease \ + the implementation of producers. Enables deduplication.", + required: true, + example: "A database commit ID" + ) + + specversion( + :string, + "The version of the CloudEvents specification which the event uses. This \ + enables the interpretation of the context. Compliant event producers \ + MUST use a value of 0.2 when referring to this version of the \ + specification.", + required: true, + example: "0.2" + ) + + source( + :string, + "This describes the event producer. Often this will include information such \ + as the type of the event source, the organization publishing the event, the \ + process that produced the event, and some unique identifiers. The exact syntax \ + and semantics behind the data encoded in the URI is event producer defined.", + required: true, + example: "/cloudevents/spec/pull/123" + ) + + type( + :string, + "Type of occurrence which has happened. Often this attribute is used for \ + routing, observability, policy enforcement, etc.", + required: true, + example: "com.example.object.delete.v2" + ) + + rig( + Schema.new do + properties do + correlation(:string, "Correlation ID", + required: true, + example: "g2dkAA1ub25vZGVAbm9ob3N0AAADxwAAAAAA" + ) + end + end + ) + end + end + } + end +end diff --git a/lib/rig_api/v2/session_blacklist.ex b/lib/rig_api/v2/session_blacklist.ex new file mode 100644 index 00000000..ef01da1b --- /dev/null +++ b/lib/rig_api/v2/session_blacklist.ex @@ -0,0 +1,191 @@ +defmodule RigApi.V2.SessionBlacklist do + @moduledoc """ + Allows for blocking "sessions" for a specific period of time. + + What a session is depends on your business context and the `JWT_SESSION_FIELD` + setting. For example, a session ID could be a random ID assigned to a token upon + login, or the id of the user the token belongs to. + + """ + use RigApi, :controller + use PhoenixSwagger + require Logger + + alias RIG.Session + + @prefix "/v2" + @cors_origins "*" + + # --- + + @doc false + def handle_preflight(%{method: "OPTIONS"} = conn, _params) do + conn + |> with_allow_origin() + |> put_resp_header("access-control-allow-methods", "POST") + |> put_resp_header("access-control-allow-headers", "content-type") + |> send_resp(:no_content, "") + end + + # --- + + defp with_allow_origin(conn) do + put_resp_header(conn, "access-control-allow-origin", @cors_origins) + end + + # --- + + swagger_path :check_status do + get(@prefix <> "/session-blacklist/{sessionId}") + summary("Check whether a given session is currently blacklisted.") + + parameters do + sessionId(:path, :string, "The JWT ID (jti) claim that identifies the session.", + required: true + ) + end + + response(200, "This session is currently blacklisted.") + response(404, "There is no entry in the blacklist that matches this session name.") + end + + @doc "Check blacklist status for a specific session id." + def check_status(%{method: "GET"} = conn, %{"session_id" => session_id}) do + if Session.blacklisted?(session_id) do + conn + |> put_resp_header("content-type", "application/json; charset=utf-8") + |> send_resp(:ok, "{}") + else + conn + |> send_resp(:not_found, "Not found.") + end + end + + # --- + + swagger_path :blacklist_session do + post(@prefix <> "/session-blacklist") + summary("Add a session to the session blacklist.") + + description(""" + When successful, the given session is no longer considered valid, regardless of \ + the token's expiration timestamp. This has the following consequences: + + - Any existing connection related to the session is terminated immediately. + - The related authorization token is no longer valid when a client establishes a connection. + - The related authorization token is no longer valid when a client creates a subscription. + """) + + parameters do + sessionBlacklist( + :body, + Schema.ref(:SessionBlacklistRequest), + "The details for blacklisting a session", + required: true + ) + end + + response( + 201, + "The session is now blacklisted. The location header points to the newly created entry." + ) + + response(400, "Bad request.") + end + + @doc "Plug action to add a session id to the session blacklist." + def blacklist_session(%{method: "POST"} = conn, _params) do + conn = with_allow_origin(conn) + + case parse(conn.body_params) do + {:error, reason} -> + send_resp(conn, :bad_request, reason) + + {:ok, %{session_id: session_id, ttl_s: ttl_s}} -> + Session.blacklist(session_id, ttl_s) + + location = Path.join(conn.request_path, "/#{session_id}") + + conn + |> put_resp_header("location", location) + |> put_resp_header("content-type", "application/json; charset=utf-8") + |> send_resp(:created, "{}") + end + end + + # --- + + defp parse(body) do + Result.ok(%{}) + |> Result.and_then(&parse_and_add_session_id(&1, body)) + |> Result.and_then(&parse_and_add_ttl_s(&1, body)) + end + + # --- + + defp parse_and_add_session_id(into, from) do + case Map.fetch(from, "sessionId") do + {:ok, value} when byte_size(value) > 0 -> {:ok, Map.merge(into, %{session_id: value})} + {:ok, value} -> {:error, "Expected non-empty string, got #{inspect(value)}"} + :error -> {:error, "Missing value for \"sessionId\""} + end + end + + # --- + + defp parse_and_add_ttl_s(into, from) do + case Map.fetch(from, "validityInSeconds") do + {:ok, value} when is_number(value) and value > 0 -> + {:ok, Map.merge(into, %{ttl_s: value})} + + {:ok, value} when byte_size(value) > 0 -> + case Integer.parse(value) do + {value, ""} -> parse_and_add_ttl_s(into, Map.put(from, "validityInSeconds", value)) + _ -> {:error, "Expected a number, got #{inspect(value)}"} + end + + {:ok, value} -> + {:error, "Expected a positive number, got #{inspect(value)}"} + + :error -> + {:error, "Missing value for \"validityInSeconds\""} + end + end + + # --- + + def swagger_definitions do + %{ + SessionBlacklistRequest: + swagger_schema do + title("Session Blacklist Request") + + properties do + sessionId( + :string, + """ + The JWT claim that defines a "session". For details see the \ + `JWT_SESSION_FIELD` setting in the operator's guide \ + (https://accenture.github.io/reactive-interaction-gateway/docs/rig-ops-guide.html). + """, + required: true + ) + + validityInSeconds( + :number, + """ + Defines how long the sessionId will stay on the blacklist. \ + Typically set to the JWT's remaining lifetime. + """, + required: true + ) + end + + example(%{ + sessionId: "SomeSessionID123", + validityInSeconds: 60 + }) + end + } + end +end diff --git a/test/blacklist_test.exs b/test/blacklist_test.exs index 5837f25b..e01fa639 100644 --- a/test/blacklist_test.exs +++ b/test/blacklist_test.exs @@ -70,7 +70,7 @@ defmodule BlacklistTest do %{validityInSeconds: "123", sessionId: "some session name"} |> Jason.encode!() - {:ok, %HTTPoison.Response{status_code: 201}} = + {:ok, %HTTPoison.Response{status_code: 200}} = HTTPoison.post("#{@rig_api_url}/v1/session-blacklist", body, [ {"content-type", "application/json"} ]) @@ -84,7 +84,7 @@ defmodule BlacklistTest do |> Jason.encode!() {:ok, %HTTPoison.Response{status_code: 201}} = - HTTPoison.post("#{@rig_api_url}/v1/session-blacklist", body, [ + HTTPoison.post("#{@rig_api_url}/v2/session-blacklist", body, [ {"content-type", "application/json"} ]) end @@ -92,11 +92,10 @@ defmodule BlacklistTest do # --- defp blacklisted?(jti) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} = - HTTPoison.get("#{@rig_api_url}/v1/session-blacklist/#{URI.encode(jti)}") - - %{"isBlacklisted" => blacklisted?} = Jason.decode!(body) - blacklisted? + case HTTPoison.get("#{@rig_api_url}/v2/session-blacklist/#{URI.encode(jti)}") do + {:ok, %HTTPoison.Response{status_code: 200}} -> true + {:ok, %HTTPoison.Response{status_code: 404}} -> false + end end # --- diff --git a/test/connection_test.exs b/test/connection_test.exs index a7c3d558..24d20edb 100644 --- a/test/connection_test.exs +++ b/test/connection_test.exs @@ -4,7 +4,6 @@ defmodule RigInboundGateway.ConnectionTest do require Logger alias HTTPoison - alias Socket.Web, as: WebSocket @dispatch Confex.fetch_env!(:rig, RigInboundGatewayWeb.Endpoint)[:https][:dispatch] @port 47_210 diff --git a/test/rig_api/controllers/health_controller_test.exs b/test/rig_api/health_controller_test.exs similarity index 100% rename from test/rig_api/controllers/health_controller_test.exs rename to test/rig_api/health_controller_test.exs diff --git a/test/rig_api/controllers/apis_controller_test.exs b/test/rig_api/v1/apis_controller_test.exs similarity index 98% rename from test/rig_api/controllers/apis_controller_test.exs rename to test/rig_api/v1/apis_controller_test.exs index 58c1e5ca..9b0e818e 100644 --- a/test/rig_api/controllers/apis_controller_test.exs +++ b/test/rig_api/v1/apis_controller_test.exs @@ -1,4 +1,4 @@ -defmodule RigApi.ApisControllerTest do +defmodule RigApi.V1.ApisControllerTest do @moduledoc false require Logger use ExUnit.Case, async: true diff --git a/test/rig_api/controllers/message_controller_test.exs b/test/rig_api/v1/message_controller_test.exs similarity index 98% rename from test/rig_api/controllers/message_controller_test.exs rename to test/rig_api/v1/message_controller_test.exs index f2cf209a..ec41c248 100644 --- a/test/rig_api/controllers/message_controller_test.exs +++ b/test/rig_api/v1/message_controller_test.exs @@ -1,4 +1,4 @@ -defmodule RigApi.MessageControllerTest do +defmodule RigApi.V1.MessageControllerTest do @moduledoc false use RigApi.ConnCase, async: true diff --git a/test/support/api_proxy_injection.ex b/test/support/api_proxy_injection.ex index 7d1c860a..10eaf14f 100644 --- a/test/support/api_proxy_injection.ex +++ b/test/support/api_proxy_injection.ex @@ -1,16 +1,21 @@ defmodule RigInboundGateway.ApiProxyInjection do @moduledoc false - @orig_val Application.get_env(:rig, RigApi.ApisController) + @mods [RigApi.V1.APIs, RigApi.V2.APIs] + @orig_vals for mod <- @mods, do: {mod, Application.get_env(:rig, mod)} def set do - Application.put_env(:rig, RigApi.ApisController, - rig_proxy: RigInboundGateway.Proxy, - persistent: true - ) + for mod <- @mods do + Application.put_env(:rig, mod, + rig_proxy: RigInboundGateway.Proxy, + persistent: true + ) + end end def restore do - Application.put_env(:rig, RigApi.ApisController, @orig_val, persistent: true) + for {mod, orig_val} <- @orig_vals do + Application.put_env(:rig, mod, orig_val, persistent: true) + end end end From bfd447826b61096302714592edc387b2de001101 Mon Sep 17 00:00:00 2001 From: Kevin Bader Date: Tue, 10 Sep 2019 10:32:39 +0200 Subject: [PATCH 3/7] Add controller tests for RIG_API --- .../rig_api/v1/apis_test.exs | 2 +- .../rig_api/v1/messages_test.exs | 2 +- lib/rig_api/v1/session_blacklist_test.exs | 23 ++++ lib/rig_api/v2/apis_test.exs | 95 +++++++++++++++ lib/rig_api/v2/messages_test.exs | 111 ++++++++++++++++++ lib/rig_api/v2/session_blacklist_test.exs | 29 +++++ test/blacklist_test.exs | 11 -- test/support/conn_case.ex | 4 - 8 files changed, 260 insertions(+), 17 deletions(-) rename test/rig_api/v1/apis_controller_test.exs => lib/rig_api/v1/apis_test.exs (98%) rename test/rig_api/v1/message_controller_test.exs => lib/rig_api/v1/messages_test.exs (98%) create mode 100644 lib/rig_api/v1/session_blacklist_test.exs create mode 100644 lib/rig_api/v2/apis_test.exs create mode 100644 lib/rig_api/v2/messages_test.exs create mode 100644 lib/rig_api/v2/session_blacklist_test.exs diff --git a/test/rig_api/v1/apis_controller_test.exs b/lib/rig_api/v1/apis_test.exs similarity index 98% rename from test/rig_api/v1/apis_controller_test.exs rename to lib/rig_api/v1/apis_test.exs index 9b0e818e..ed61f42d 100644 --- a/test/rig_api/v1/apis_controller_test.exs +++ b/lib/rig_api/v1/apis_test.exs @@ -1,4 +1,4 @@ -defmodule RigApi.V1.ApisControllerTest do +defmodule RigApi.V1.APIsTest do @moduledoc false require Logger use ExUnit.Case, async: true diff --git a/test/rig_api/v1/message_controller_test.exs b/lib/rig_api/v1/messages_test.exs similarity index 98% rename from test/rig_api/v1/message_controller_test.exs rename to lib/rig_api/v1/messages_test.exs index ec41c248..4625e0ba 100644 --- a/test/rig_api/v1/message_controller_test.exs +++ b/lib/rig_api/v1/messages_test.exs @@ -1,4 +1,4 @@ -defmodule RigApi.V1.MessageControllerTest do +defmodule RigApi.V1.MessagesTest do @moduledoc false use RigApi.ConnCase, async: true diff --git a/lib/rig_api/v1/session_blacklist_test.exs b/lib/rig_api/v1/session_blacklist_test.exs new file mode 100644 index 00000000..eec85530 --- /dev/null +++ b/lib/rig_api/v1/session_blacklist_test.exs @@ -0,0 +1,23 @@ +defmodule RigApi.V1.SessionBlacklistTest do + @moduledoc false + use RigApi.ConnCase, async: true + + alias UUID + + @prefix "/v1" + + test "After blacklisting a session ID, the blacklist entry is present." do + session_id = UUID.uuid4() + + body = Jason.encode!(%{validityInSeconds: "123", sessionId: session_id}) + + build_conn() + |> put_req_header("content-type", "application/json") + |> post(@prefix <> "/session-blacklist", body) + |> json_response(200) + + build_conn() + |> get(@prefix <> "/session-blacklist/#{session_id}") + |> json_response(200) + end +end diff --git a/lib/rig_api/v2/apis_test.exs b/lib/rig_api/v2/apis_test.exs new file mode 100644 index 00000000..131355cb --- /dev/null +++ b/lib/rig_api/v2/apis_test.exs @@ -0,0 +1,95 @@ +defmodule RigApi.V2.APIsTest do + @moduledoc false + require Logger + use ExUnit.Case, async: true + use RigApi.ConnCase + + describe "GET /v2/apis" do + test "should return list of APIs and filter deactivated APIs" do + conn = build_conn() |> get("/v2/apis") + assert json_response(conn, 200) |> length == 1 + end + end + + describe "GET /v2/apis/:id" do + test "should return requested API" do + conn = build_conn() |> get("/v2/apis/new-service") + response = json_response(conn, 200) + assert response["id"] == "new-service" + end + + test "should 404 if requested API doesn't exist" do + conn = build_conn() |> get("/v2/apis/fake-service") + response = json_response(conn, 404) + assert response["message"] == "API with id=fake-service doesn't exists." + end + + test "should return 404 if API exists, but is deactivated" do + conn = build_conn() |> get("/v2/apis/another-service") + response = json_response(conn, 404) + assert response["message"] == "API with id=another-service doesn't exists." + end + end + + describe "POST /v2/apis" do + test "should add new API" do + new_api = @mock_api |> Map.put("id", "different-id") + conn = build_conn() |> post("/v2/apis", new_api) + response = json_response(conn, 201) + assert response["message"] == "ok" + end + + test "should return 409 if API already exist" do + conn = build_conn() |> post("/v2/apis", @mock_api) + response = json_response(conn, 409) + assert response["message"] == "API with id=new-service already exists." + end + + test "should replaced deactivated API with same ID" do + new_api = @mock_api |> Map.put("id", "another-service") + conn = build_conn() |> post("/v2/apis", new_api) + response = json_response(conn, 201) + assert response["message"] == "ok" + end + end + + describe "PUT /v2/apis/:id" do + test "should update requested API" do + conn = build_conn() |> put("/v2/apis/new-service", @mock_api) + response = json_response(conn, 200) + assert response["message"] == "ok" + end + + test "should return 404 if requested API doesn't exist" do + conn = build_conn() |> put("/v2/apis/fake-service", %{}) + response = json_response(conn, 404) + assert response["message"] == "API with id=fake-service doesn't exists." + end + + test "should return 404 if API exists, but is deactivated" do + conn = build_conn() |> put("/v2/apis/another-service", @mock_api) + response = json_response(conn, 404) + assert response["message"] == "API with id=another-service doesn't exists." + end + end + + describe "DELETE /v2/apis/:id" do + test "should delete requested API" do + conn = build_conn() |> delete("/v2/apis/new-service") + response = json_response(conn, 204) + assert response == %{} + end + + test "should return 404 if requested API doesn't exist" do + conn = build_conn() |> delete("/v2/apis/fake-service") + response = json_response(conn, 404) + assert response["message"] == "API with id=fake-service doesn't exists." + end + + test "should return 404 if API exists, but is deactivated" do + conn = build_conn() |> delete("/v2/apis/another-service") + response = json_response(conn, 404) + assert response["message"] == "API with id=another-service doesn't exists." + end + end +end diff --git a/lib/rig_api/v2/messages_test.exs b/lib/rig_api/v2/messages_test.exs new file mode 100644 index 00000000..eeb689ef --- /dev/null +++ b/lib/rig_api/v2/messages_test.exs @@ -0,0 +1,111 @@ +defmodule RigApi.V2.MessagesTest do + @moduledoc false + use RigApi.ConnCase, async: true + + alias Plug.Conn.Status + + import Mox + setup :verify_on_exit! + + @cloud_event_json ~s({"cloudEventsVersion":"0.1","source":"test","eventType":"test.event","eventID":"1"}) + @non_cloud_event ~s({"source":"test","eventType":"test.event","eventID":"1"}) + + setup do + # TODO that mock doesn't work - the request goes to the real Filter :( + Rig.EventFilterMock + |> stub(:forward_event, fn ev -> send(self(), {:cloud_event_sent, ev}) end) + + :ok + end + + # test "application/x-www-form-urlencoded is not supported" + # test "text/plain is not supported" + + describe "Structured mode" do + test "is supported for content-type application/cloudevents+json." do + conn = + new_conn("application/cloudevents+json;charset=utf-8") + |> post("/v2/messages", @cloud_event_json) + + assert conn.status == Status.code(:accepted) + # TODO mock doesn't work.. + # assert_receive {:cloud_event_sent, _} + end + + test "ignores any ce-* headers." do + conn = + new_conn("application/cloudevents+json;charset=utf-8") + |> put_req_header("ce-specversion", "illegal") + |> put_req_header("ce-type", "") + |> put_req_header("ce-source", "") + |> put_req_header("ce-id", "") + |> post("/v2/messages", @cloud_event_json) + + assert conn.status == Status.code(:accepted), "#{conn.status} #{inspect(conn.resp_body)}" + # TODO mock doesn't work.. + # assert_receive {:cloud_event_sent, @cloud_event_json} + end + + test "rejects events that don't follow the CloudEvents format." do + conn = + new_conn("application/cloudevents+json;charset=utf-8") + |> post("/v2/messages", @non_cloud_event) + + assert conn.status == Status.code(:bad_request) + refute_received {:cloud_event_sent, _} + end + end + + describe "Binary content mode" do + test "is supported for content-type application/json." do + event = %{ + "specversion" => "0.2", + "type" => "my-event-type", + "source" => "#{__MODULE__}/binary/json", + "id" => "1", + "contenttype" => "application/json;charset=utf-8", + "data" => ~S({"some": "value"}) + } + + conn = + new_conn(event["contenttype"]) + |> put_req_header("ce-specversion", event["specversion"]) + |> put_req_header("ce-type", event["type"]) + |> put_req_header("ce-source", event["source"]) + |> put_req_header("ce-id", event["id"]) + |> post("/v2/messages", event["data"]) + + assert conn.status == Status.code(:accepted) + # TODO mock doesn't work.. + # assert_receive {:cloud_event_sent, event} + end + + test "is not supported for any other content-type." do + event = %{ + "specversion" => "0.2", + "type" => "my-event-type", + "source" => "#{__MODULE__}/binary/text", + "id" => "1", + "contenttype" => "text/plain", + "data" => "This is a human-readable representation.." + } + + conn = + new_conn(event["contenttype"]) + |> put_req_header("ce-specversion", event["specversion"]) + |> put_req_header("ce-type", event["type"]) + |> put_req_header("ce-source", event["source"]) + |> put_req_header("ce-id", event["id"]) + |> post("/v2/messages", event["data"]) + + assert conn.status == Status.code(:accepted) + # TODO mock doesn't work.. + # assert_receive {:cloud_event_sent, event} + end + end + + defp new_conn(content_type) do + build_conn() + |> put_req_header("content-type", content_type) + end +end diff --git a/lib/rig_api/v2/session_blacklist_test.exs b/lib/rig_api/v2/session_blacklist_test.exs new file mode 100644 index 00000000..5ac7a53c --- /dev/null +++ b/lib/rig_api/v2/session_blacklist_test.exs @@ -0,0 +1,29 @@ +defmodule RigApi.V2.SessionBlacklistTest do + @moduledoc false + use RigApi.ConnCase, async: true + + alias UUID + + @prefix "/v2" + + test "After blacklisting a session ID, the location header points to the blacklist entry." do + session_id = UUID.uuid4() + + body = Jason.encode!(%{validityInSeconds: 123, sessionId: session_id}) + + conn = + build_conn() + |> put_req_header("content-type", "application/json") + |> post(@prefix <> "/session-blacklist", body) + + # Assert 201 and json response: + json_response(conn, 201) + + [entry_location] = get_resp_header(conn, "location") + + # We know it's an absolute-path reference, so we can use build_conn to fetch it. + build_conn() + |> get(entry_location) + |> json_response(200) + end +end diff --git a/test/blacklist_test.exs b/test/blacklist_test.exs index e01fa639..27b02174 100644 --- a/test/blacklist_test.exs +++ b/test/blacklist_test.exs @@ -65,17 +65,6 @@ defmodule BlacklistTest do end end - test "In RIG 2.x, the session API supports passing validityInSeconds as a string." do - body = - %{validityInSeconds: "123", sessionId: "some session name"} - |> Jason.encode!() - - {:ok, %HTTPoison.Response{status_code: 200}} = - HTTPoison.post("#{@rig_api_url}/v1/session-blacklist", body, [ - {"content-type", "application/json"} - ]) - end - # --- defp blacklist(session_id) do diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 2eefffa2..23b60dfc 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -57,8 +57,4 @@ defmodule RigApi.ConnCase do @endpoint RigApi.Endpoint end end - - setup _tags do - {:ok, conn: Phoenix.ConnTest.build_conn()} - end end From fe00223d332355e75701675d7bceb03f5e607ac5 Mon Sep 17 00:00:00 2001 From: Kevin Bader Date: Tue, 10 Sep 2019 10:38:14 +0200 Subject: [PATCH 4/7] Use API v2 in docs and i9n tests --- docs/api-gateway-management.md | 10 +++++----- docs/api-gateway.md | 2 +- examples/channels-example/README.md | 2 +- examples/channels-example/run-compose.sh | 2 +- .../proxy/publish_to_event_stream/kafka_test.exs | 4 ++-- test/rig_tests/proxy/request_logger/kafka_test.exs | 4 ++-- test/rig_tests/proxy/response_from/async_http_test.exs | 6 +++--- test/rig_tests/proxy/response_from/http_test.exs | 2 +- test/rig_tests/proxy/response_from/kafka_test.exs | 2 +- test/rig_tests/proxy/response_from/kinesis_test.exs | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/api-gateway-management.md b/docs/api-gateway-management.md index a887d79b..073f2023 100644 --- a/docs/api-gateway-management.md +++ b/docs/api-gateway-management.md @@ -23,7 +23,7 @@ You'll right away see list of all internal APIs. ## Create new API -`POST /v1/apis` +`POST /v2/apis` ```json { @@ -59,17 +59,17 @@ You'll right away see list of all internal APIs. ## Read list of APIs -`GET /v1/apis` +`GET /v2/apis` This is also way how to check if your APIs were loaded properly. ## Read detail of specific API -`GET /v1/apis/:api_id` +`GET /v2/apis/:api_id` ## Update API -`PUT /v1/apis/:api_id` +`PUT /v2/apis/:api_id` ```json { @@ -105,4 +105,4 @@ This is also way how to check if your APIs were loaded properly. ## Delete API -`DELETE /v1/apis/:api_id` +`DELETE /v2/apis/:api_id` diff --git a/docs/api-gateway.md b/docs/api-gateway.md index 62fa582a..3270dacb 100644 --- a/docs/api-gateway.md +++ b/docs/api-gateway.md @@ -194,7 +194,7 @@ Configuration of such API endpoint might look like this: > Note the presence of `response_from` field. This tells RIG to wait for different event with the same correlation ID. -As an alternative you can set `response_from` to `http_async`. This means that correlated response has to be sent to internal `:4010/v1/responses` `POST` endpoint with a body like this: +As an alternative you can set `response_from` to `http_async`. This means that correlated response has to be sent to internal `:4010/v2/responses` `POST` endpoint with a body like this: ```json { diff --git a/examples/channels-example/README.md b/examples/channels-example/README.md index a23d7060..a02cde70 100644 --- a/examples/channels-example/README.md +++ b/examples/channels-example/README.md @@ -79,7 +79,7 @@ curl -X "POST" \ -H "Content-Type: application/json" \ -d "{\"id\":\"kafka-service\",\"name\":\"kafka-service\",\"version_data\":{\"default\":{\"endpoints\":[{\"id\":\"kafka-producer-endpoint\",\"path\":\"/produce\",\"method\":\"POST\",\"secured\":false}]}},\"proxy\":{\"use_env\":false,\"target_url\":\"localhost\",\"port\":8000}}" \ --silent \ -"http://localhost:7010/v1/apis" +"http://localhost:7010/v2/apis" ``` ## Example scenarios to test diff --git a/examples/channels-example/run-compose.sh b/examples/channels-example/run-compose.sh index c7c96720..41972838 100755 --- a/examples/channels-example/run-compose.sh +++ b/examples/channels-example/run-compose.sh @@ -23,6 +23,6 @@ curl -X "POST" \ -H "Content-Type: application/json" \ -d "{\"id\":\"kafka-service\",\"name\":\"kafka-service\",\"version_data\":{\"default\":{\"endpoints\":[{\"id\":\"kafka-producer-endpoint\",\"path\":\"/produce\",\"method\":\"POST\",\"secured\":false}]}},\"proxy\":{\"use_env\":false,\"target_url\":\"channels-external-service\",\"port\":8000}}" \ --silent \ -"http://localhost:7010/v1/apis" +"http://localhost:7010/v2/apis" printf "\n===> Application is ready <===\n" diff --git a/test/rig_tests/proxy/publish_to_event_stream/kafka_test.exs b/test/rig_tests/proxy/publish_to_event_stream/kafka_test.exs index c0ef8d55..9ba97e40 100644 --- a/test/rig_tests/proxy/publish_to_event_stream/kafka_test.exs +++ b/test/rig_tests/proxy/publish_to_event_stream/kafka_test.exs @@ -62,7 +62,7 @@ defmodule RigTests.Proxy.PublishToEventStream.KafkaTest do endpoint_path = "/#{endpoint_id}" # We register the endpoint with the proxy: - rig_api_url = "http://localhost:#{@api_port}/v1/apis" + rig_api_url = "http://localhost:#{@api_port}/v2/apis" rig_proxy_url = "http://localhost:#{@proxy_port}" body = @@ -121,7 +121,7 @@ defmodule RigTests.Proxy.PublishToEventStream.KafkaTest do endpoint_path = "/#{endpoint_id}" # We register the endpoint with the proxy: - rig_api_url = "http://localhost:#{@api_port}/v1/apis" + rig_api_url = "http://localhost:#{@api_port}/v2/apis" rig_proxy_url = "http://localhost:#{@proxy_port}" setup_req_body = diff --git a/test/rig_tests/proxy/request_logger/kafka_test.exs b/test/rig_tests/proxy/request_logger/kafka_test.exs index 85ff194e..700e33cc 100644 --- a/test/rig_tests/proxy/request_logger/kafka_test.exs +++ b/test/rig_tests/proxy/request_logger/kafka_test.exs @@ -67,7 +67,7 @@ defmodule RigTests.Proxy.RequestLogger.KafkaTest do endpoint_path = "/#{endpoint_id}" # We register the endpoint with the proxy: - rig_api_url = "http://localhost:#{@api_port}/v1/apis" + rig_api_url = "http://localhost:#{@api_port}/v2/apis" rig_proxy_url = "http://localhost:#{@proxy_port}" route(endpoint_path, Response.ok!(~s<{"status":"ok"}>)) @@ -134,7 +134,7 @@ defmodule RigTests.Proxy.RequestLogger.KafkaTest do endpoint_path = "/#{endpoint_id}" # We register the endpoint with the proxy: - rig_api_url = "http://localhost:#{@api_port}/v1/apis" + rig_api_url = "http://localhost:#{@api_port}/v2/apis" rig_proxy_url = "http://localhost:#{@proxy_port}" route(endpoint_path, Response.ok!(~s<{"status":"ok"}>)) diff --git a/test/rig_tests/proxy/response_from/async_http_test.exs b/test/rig_tests/proxy/response_from/async_http_test.exs index 48cb092e..112d673b 100644 --- a/test/rig_tests/proxy/response_from/async_http_test.exs +++ b/test/rig_tests/proxy/response_from/async_http_test.exs @@ -1,6 +1,6 @@ defmodule RigTests.Proxy.ResponseFrom.AsyncHttpTest do @moduledoc """ - If `response_from` is set to http_async, the response is taken from internal HTTP endpoint /v1/responses + If `response_from` is set to http_async, the response is taken from internal HTTP endpoint /v2/responses Note that `test_with_server` sets up an HTTP server mock, which is then configured using the `route` macro. @@ -49,13 +49,13 @@ defmodule RigTests.Proxy.ResponseFrom.AsyncHttpTest do build_conn() |> put_req_header("content-type", "application/json;charset=utf-8") - |> post("/v1/responses", event) + |> post("/v2/responses", event) Response.ok!(sync_response, %{"content-type" => "application/json"}) end) # We register the endpoint with the proxy: - rig_api_url = "http://localhost:#{@api_port}/v1/apis" + rig_api_url = "http://localhost:#{@api_port}/v2/apis" rig_proxy_url = "http://localhost:#{@proxy_port}" body = diff --git a/test/rig_tests/proxy/response_from/http_test.exs b/test/rig_tests/proxy/response_from/http_test.exs index f50ada1b..1b35e3ae 100644 --- a/test/rig_tests/proxy/response_from/http_test.exs +++ b/test/rig_tests/proxy/response_from/http_test.exs @@ -38,7 +38,7 @@ defmodule RigTests.Proxy.ResponseFrom.HttpTest do ) # We register the endpoint with the proxy: - rig_api_url = "http://localhost:#{@api_port}/v1/apis" + rig_api_url = "http://localhost:#{@api_port}/v2/apis" rig_proxy_url = "http://localhost:#{@proxy_port}" body = diff --git a/test/rig_tests/proxy/response_from/kafka_test.exs b/test/rig_tests/proxy/response_from/kafka_test.exs index 8272074e..db5661c3 100644 --- a/test/rig_tests/proxy/response_from/kafka_test.exs +++ b/test/rig_tests/proxy/response_from/kafka_test.exs @@ -82,7 +82,7 @@ defmodule RigTests.Proxy.ResponseFrom.KafkaTest do end) # We register the endpoint with the proxy: - rig_api_url = "http://localhost:#{@api_port}/v1/apis" + rig_api_url = "http://localhost:#{@api_port}/v2/apis" rig_proxy_url = "http://localhost:#{@proxy_port}" body = diff --git a/test/rig_tests/proxy/response_from/kinesis_test.exs b/test/rig_tests/proxy/response_from/kinesis_test.exs index 635a0aa5..018b4d7e 100644 --- a/test/rig_tests/proxy/response_from/kinesis_test.exs +++ b/test/rig_tests/proxy/response_from/kinesis_test.exs @@ -58,7 +58,7 @@ defmodule RigTests.Proxy.ResponseFrom.KinesisTest do end) # We register the endpoint with the proxy: - rig_api_url = "http://localhost:#{@api_port}/v1/apis" + rig_api_url = "http://localhost:#{@api_port}/v2/apis" rig_proxy_url = "http://localhost:#{@proxy_port}" body = From 836e5e359a18ec326872fd9919733d314287c0a3 Mon Sep 17 00:00:00 2001 From: Kevin Bader Date: Tue, 10 Sep 2019 10:49:58 +0200 Subject: [PATCH 5/7] Add `moduledoc`s to the API controllers --- lib/rig_api/health.ex | 1 + lib/rig_api/v1/apis.ex | 5 +---- lib/rig_api/v1/messages.ex | 1 + lib/rig_api/v1/responses.ex | 1 + lib/rig_api/v1/session_blacklist.ex | 3 +-- lib/rig_api/v2/apis.ex | 5 +---- lib/rig_api/v2/messages.ex | 1 + lib/rig_api/v2/responses.ex | 1 + lib/rig_api/v2/session_blacklist.ex | 3 +-- 9 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/rig_api/health.ex b/lib/rig_api/health.ex index 2f3b7d3b..5b91b26a 100644 --- a/lib/rig_api/health.ex +++ b/lib/rig_api/health.ex @@ -1,4 +1,5 @@ defmodule RigApi.Health do + @moduledoc "Controller for the health endpoint." require Logger use RigApi, :controller diff --git a/lib/rig_api/v1/apis.ex b/lib/rig_api/v1/apis.ex index 59645d21..f8101754 100644 --- a/lib/rig_api/v1/apis.ex +++ b/lib/rig_api/v1/apis.ex @@ -1,8 +1,5 @@ defmodule RigApi.V1.APIs do - @moduledoc """ - HTTP-accessible API for managing PROXY APIs. - - """ + @moduledoc "CRUD controller for the reverse-proxy settings." use Rig.Config, [:rig_proxy] use RigApi, :controller use PhoenixSwagger diff --git a/lib/rig_api/v1/messages.ex b/lib/rig_api/v1/messages.ex index a30c90c9..834f49ba 100644 --- a/lib/rig_api/v1/messages.ex +++ b/lib/rig_api/v1/messages.ex @@ -1,4 +1,5 @@ defmodule RigApi.V1.Messages do + @moduledoc "Controller for submitting (backend) events to potential (frontend) subscribers." require Logger use RigApi, :controller diff --git a/lib/rig_api/v1/responses.ex b/lib/rig_api/v1/responses.ex index 8303d58b..76382ebf 100644 --- a/lib/rig_api/v1/responses.ex +++ b/lib/rig_api/v1/responses.ex @@ -1,4 +1,5 @@ defmodule RigApi.V1.Responses do + @moduledoc "Controller for submitting (backend) responses to asynchronous (frontend) requests." require Logger use RigApi, :controller diff --git a/lib/rig_api/v1/session_blacklist.ex b/lib/rig_api/v1/session_blacklist.ex index 7114ae50..1848e75f 100644 --- a/lib/rig_api/v1/session_blacklist.ex +++ b/lib/rig_api/v1/session_blacklist.ex @@ -1,11 +1,10 @@ defmodule RigApi.V1.SessionBlacklist do @moduledoc """ - Allows for blocking "sessions" for a specific period of time. + Controller that allows blocking "sessions" for a specific period of time. What a session is depends on your business context and the `JWT_SESSION_FIELD` setting. For example, a session ID could be a random ID assigned to a token upon login, or the id of the user the token belongs to. - """ use RigApi, :controller use PhoenixSwagger diff --git a/lib/rig_api/v2/apis.ex b/lib/rig_api/v2/apis.ex index 5e1a784b..b7fc0dd9 100644 --- a/lib/rig_api/v2/apis.ex +++ b/lib/rig_api/v2/apis.ex @@ -1,8 +1,5 @@ defmodule RigApi.V2.APIs do - @moduledoc """ - HTTP-accessible API for managing PROXY APIs. - - """ + @moduledoc "CRUD controller for the reverse-proxy settings." use Rig.Config, [:rig_proxy] use RigApi, :controller use PhoenixSwagger diff --git a/lib/rig_api/v2/messages.ex b/lib/rig_api/v2/messages.ex index c91895d1..c0eb96ee 100644 --- a/lib/rig_api/v2/messages.ex +++ b/lib/rig_api/v2/messages.ex @@ -1,4 +1,5 @@ defmodule RigApi.V2.Messages do + @moduledoc "Controller for submitting (backend) events to potential (frontend) subscribers." require Logger use RigApi, :controller diff --git a/lib/rig_api/v2/responses.ex b/lib/rig_api/v2/responses.ex index 4c1b0801..62a76f42 100644 --- a/lib/rig_api/v2/responses.ex +++ b/lib/rig_api/v2/responses.ex @@ -1,4 +1,5 @@ defmodule RigApi.V2.Responses do + @moduledoc "Controller for submitting (backend) responses to asynchronous (frontend) requests." require Logger use RigApi, :controller diff --git a/lib/rig_api/v2/session_blacklist.ex b/lib/rig_api/v2/session_blacklist.ex index ef01da1b..1919e706 100644 --- a/lib/rig_api/v2/session_blacklist.ex +++ b/lib/rig_api/v2/session_blacklist.ex @@ -1,11 +1,10 @@ defmodule RigApi.V2.SessionBlacklist do @moduledoc """ - Allows for blocking "sessions" for a specific period of time. + Controller that allows blocking "sessions" for a specific period of time. What a session is depends on your business context and the `JWT_SESSION_FIELD` setting. For example, a session ID could be a random ID assigned to a token upon login, or the id of the user the token belongs to. - """ use RigApi, :controller use PhoenixSwagger From f99021e650e40a55ac62814db92e64b51518c39a Mon Sep 17 00:00:00 2001 From: Kevin Bader Date: Tue, 17 Sep 2019 15:08:41 +0200 Subject: [PATCH 6/7] Remove unnecessary function in blacklist_test --- test/blacklist_test.exs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/test/blacklist_test.exs b/test/blacklist_test.exs index 27b02174..9e4b02e2 100644 --- a/test/blacklist_test.exs +++ b/test/blacklist_test.exs @@ -21,7 +21,7 @@ defmodule BlacklistTest do blacklist(session_id) # try to connect and verify it doesn't work - jwt = new_jwt(%{"jti" => session_id}) + jwt = JWT.encode(%{"jti" => session_id}) assert {:error, %{code: 400}} = SseClient.try_connect_then_disconnect(jwt: jwt) assert {:error, _} = WsClient.try_connect_then_disconnect(jwt: jwt) end @@ -30,7 +30,7 @@ defmodule BlacklistTest do # Connect to RIG using a JWT: session_id = "some random string 8902731973190231212" - jwt = new_jwt(%{"jti" => session_id}) + jwt = JWT.encode(%{"jti" => session_id}) assert {:ok, sse} = SseClient.connect(jwt: jwt) {_, sse} = SseClient.read_welcome_event(sse) @@ -43,7 +43,7 @@ defmodule BlacklistTest do # Create an additional connection using a different JWT: other_session_id = "some random string 97123689684290890423312" - other_jwt = new_jwt(%{"jti" => other_session_id}) + other_jwt = JWT.encode(%{"jti" => other_session_id}) assert {:ok, other_sse} = SseClient.connect(jwt: other_jwt) {_, other_sse} = SseClient.read_welcome_event(other_sse) @@ -86,10 +86,4 @@ defmodule BlacklistTest do {:ok, %HTTPoison.Response{status_code: 404}} -> false end end - - # --- - - defp new_jwt(claims) do - JWT.encode(claims) - end end From 6f06733881de14710d26db8ff54eae961c4609f5 Mon Sep 17 00:00:00 2001 From: Kevin Bader Date: Tue, 17 Sep 2019 17:17:38 +0200 Subject: [PATCH 7/7] Add WS related info on what is sent in case of :session_killed --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f84b028..093d704e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added longpolling as new connection type [#217](https://github.com/Accenture/reactive-interaction-gateway/issues/217) -- When terminating an SSE connection after its associated session has been blacklisted, RIG now sends out a `rig.session_killed` event before closing the socket. +- When terminating an SSE connection after its associated session has been blacklisted, RIG now sends out a `rig.session_killed` event before closing the socket. For WebSocket connections, the closing frame contains "Session killed." as its payload. - New API for querying and updating the session blacklist: `/v2/session-blacklist`, which introduces the following breaking changes: - When a session has been added to the session blacklist successfully, the endpoint now uses the correct HTTP status code "201 Created" instead of "200 Ok". - When using the API to blacklist a session, the `validityInSeconds` should now be passed as an integer value (using a string still works though).