Skip to content

Commit

Permalink
feat: added api and auth modules
Browse files Browse the repository at this point in the history
  • Loading branch information
gabheadz committed Dec 12, 2023
1 parent 265e2b0 commit 9b40105
Show file tree
Hide file tree
Showing 33 changed files with 1,668 additions and 1 deletion.
4 changes: 4 additions & 0 deletions channel-bridge/apps/bridge_restapi/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
26 changes: 26 additions & 0 deletions channel-bridge/apps/bridge_restapi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
bridge_restapi-*.tar

# Temporary files, for example, from tests.
/tmp/
16 changes: 16 additions & 0 deletions channel-bridge/apps/bridge_restapi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# BridgeApi

Channel Sender Rest Api Operations.

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `bridge_restapi` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:bridge_restapi, "~> 0.1.0"}
]
end
```
27 changes: 27 additions & 0 deletions channel-bridge/apps/bridge_restapi/lib/bridge_api.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule BridgeApi do
@moduledoc """
Documentation for `ChannelBridgeApi`.
"""

use Application

alias BridgeApi.Rest.BaseRouter

@doc false
@impl Application
def start(_type, _args) do
children = [
{Plug.Cowboy,
scheme: :http,
plug: BaseRouter,
options: [
port: BridgeHelperConfig.get([:bridge, "port"], 8080),
protocol_options: [max_keepalive: 2_000, active_n: 200]
]}
]

opts = [strategy: :one_for_one, name: BridgeApi.Supervisor]
Supervisor.start_link(children, opts)
end

end
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
defmodule BridgeApi.Rest.AuthPlug do
@moduledoc false

alias BridgeApi.Rest.Header

require Logger

@type token() :: String.t()
@type conn() :: Plug.Conn.t()

defmodule NoCredentialsError do
@moduledoc """
Error raised when no credentials are sent in request
"""

defexception message: ""
end

import Plug.Conn

def init(options), do: options

@doc """
Performs authentication. The concrete auth is implemented via @behaviour 'AuthProvider', and
defined in configuration env property :channel_authenticator.
"""
def call(conn, _opts) do

auth_provider = case get_in(Application.get_env(:channel_bridge, :config), [:bridge, "channel_authenticator"]) do
nil -> BridgeRestapiAuth.PassthroughProvider
v -> String.to_existing_atom(v)
end

with {:ok, all_headers} <- Header.all_headers(conn),
{:ok, claims} <- auth_provider.validate_credentials(all_headers) do
# auth was successful and claims are stored
store_claims_private(claims, conn)
else
{:error, :nocreds} ->
Logger.error("Credentials required for authentication")
raise NoCredentialsError, message: "Credentials required for authentication"
end
end

defp store_claims_private(claims, conn) do
put_private(conn, :token_claims, claims)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule BridgeApi.Rest.BaseRouter do
@moduledoc """
"""

use Plug.Router
use Plug.ErrorHandler

plug(:match)
plug(:dispatch)

forward("/api/v1/channel-bridge-ex", to: BridgeApi.Rest.RestRouter)

match(_, do: send_resp(conn, 404, "Resource not found"))

end
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
defmodule BridgeApi.Rest.ChannelRequest do
defstruct ~w[req_headers req_params body token_claims]a

@moduledoc """
A new channel request data
"""
alias BridgeCore.Utils.JsonSearch
alias BridgeCore.AppClient
alias BridgeCore.User
alias BridgeCore.CloudEvent.Extractor

require Logger

@default_sessionid_search_path "$.req_headers['sub']"
@default_app_ref "default_app"
@default_user_ref "default_user"

@type headers_map() :: map()
@type params_map() :: map()
@type body_map() :: map()
@type claims_map() :: map()

@type t() :: %__MODULE__{
req_headers: headers_map(),
req_params: params_map(),
body: body_map(),
token_claims: claims_map()
}

@doc """
Creates a Channel Request by consolidating request data.
"""
@spec new(headers_map(), params_map(), body_map(), claims_map()) :: t()
def new(req_headers, req_params, body, token_claims) do
%__MODULE__{
req_headers: req_headers,
req_params: req_params,
body: body,
token_claims: token_claims
}
end

@spec extract_channel_alias(t()) :: {:ok, binary()} | {:error, any()}
def extract_channel_alias(request_data) do

request_channel_alias =
BridgeHelperConfig.get([:bridge, "request_channel_identifier"], @default_sessionid_search_path)

ch_alias =
request_data
|> JsonSearch.prepare()
|> JsonSearch.extract(request_channel_alias)

case ch_alias do
e when e in [nil, "undefined"] -> {:error, :nosessionidfound}
_ -> {:ok, ch_alias}
end
end

@spec extract_application(t()) :: {:ok, AppClient.t()} | {:error, any()}
def extract_application(request_data) do
app_key = BridgeHelperConfig.get([:bridge, "request_app_identifier"], @default_app_ref)
|> (fn x ->
case String.starts_with?(x, "$.") do
true -> {:lookup, x}
false -> {:fixed, x}
end
end).()

app =
case apply_strategy(app_key, request_data) do
{:ok, app_id} ->
AppClient.new(app_id, "")

{:error, err} ->
Logger.warning(
"missing application info in request, #{inspect(app_key)} = #{err}. Data: #{inspect(request_data)}"
)

AppClient.new(@default_app_ref, "")
end

{:ok, app}
end

@spec extract_user_info(t()) :: {:ok, User.t()} | {:error, any()}
def extract_user_info(request_data) do
user_key = BridgeHelperConfig.get([:bridge, "request_user_identifier"], @default_user_ref)
|> (fn x ->
case String.starts_with?(x, "$.") do
true -> {:lookup, x}
false -> {:fixed, x}
end
end).()

user =
case apply_strategy(user_key, request_data) do
{:ok, user_id} ->
User.new(user_id)

{:error, err} ->
Logger.warning(
"missing user info in request, #{inspect(user_key)} = #{err}. Data: #{inspect(request_data)}"
)

User.new(@default_app_ref)
end

{:ok, user}

end

defp apply_strategy(config, data) do
case config do
{:fixed, fixed_value} ->
# uses a fixed string value as application reference
{:ok, fixed_value}

{:lookup, key_to_search} ->
# searches for a key in user data map
Extractor.extract(data, key_to_search)
end
end

end
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule BridgeApi.Rest.ErrorResponse do
@moduledoc """
Error definition
"""

@type reason() :: String.t()
@type domain() :: String.t()
@type code() :: String.t()
@type message() :: String.t()
@type type() :: String.t()

@type t() :: %__MODULE__{
reason: reason(),
domain: domain(),
code: code(),
message: message(),
type: type()
}

@derive Jason.Encoder
defstruct reason: nil,
domain: nil,
code: nil,
message: nil,
type: nil

@doc """
creates a simple error representation
"""
@spec new(reason(), domain(), code(), message(), type()) :: t()
def new(reason, domain, code, message, type) do
%__MODULE__{
reason: reason,
domain: domain,
code: code,
message: message,
type: type
}
end
end
35 changes: 35 additions & 0 deletions channel-bridge/apps/bridge_restapi/lib/bridge_api/rest/header.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule BridgeApi.Rest.Header do
@moduledoc """
Helper Function for extractig header values
"""
import Plug.Conn

@type header_name :: String.t()
@type header_value :: String.t()
@type headers_map :: map()
@type conn :: %Plug.Conn{}

@spec get_header(conn(), header_name()) :: {:error, :notfound} | {:ok, header_value()}
def get_header(conn, header_name) do
case get_req_header(conn, header_name) do
[] ->
{:error, :notfound}

[header] ->
{:ok, header}
end
end

@spec get_auth_header(conn()) :: {:error, :nocreds} | {:ok, header_value()}
def get_auth_header(conn) do
case get_header(conn, "authorization") do
{:error, :notfound} -> {:error, :nocreds}
{:ok, header} -> {:ok, header |> String.split() |> List.last()}
end
end

@spec all_headers(conn()) :: {:ok, headers_map()}
def all_headers(conn) do
{:ok, Enum.into(conn.req_headers, %{})}
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule BridgeApi.Rest.Health.Probe do
@callback readiness() :: :ok | :error
@callback liveness() :: :ok | :error

def liveness, do: :ok
def readiness, do: :ok
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
defmodule BridgeApi.Rest.PrometheusExporter do
@moduledoc false
use Prometheus.PlugExporter
end
Loading

0 comments on commit 9b40105

Please sign in to comment.