Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/rest_api/lib/plugs/authentication.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule RestAPI.Plugs.Authentication do
@moduledoc """
Provides authentication for public calls.
Provides authentication for public and admin calls.
"""

require Logger
Expand Down
46 changes: 46 additions & 0 deletions apps/rest_api/lib/plugs/authorization.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
defmodule RestAPI.Plugs.Authorization do
@moduledoc """
Provides authorization for public and admin calls.
"""

require Logger

alias RestAPI.Controllers.Fallback
alias RestAPI.Ports.Authorizer

@behaviour Plug

@impl true
def init(opts), do: opts

@impl true
def call(%Plug.Conn{private: private} = conn, opts) when is_list(opts) do
with {:authenticated?, true} <- {:authenticated?, has_session?(private)},
{:authorized?, true} <- {:authorized?, authorized?(conn, opts[:type])} do
conn
else
{:authenticated?, false} ->
Logger.info("Session not found")
Fallback.call(conn, {:error, :unauthorized})

{:authorized?, false} ->
Logger.info("Authorization failed in some policy")
Fallback.call(conn, {:error, :unauthorized})
end
end

defp has_session?(%{session: session}) when is_map(session), do: true
defp has_session?(_any), do: false

defp authorized?(conn, "admin") do
conn
|> Authorizer.authorize_admin()
|> case do
:ok -> true
{:error, :unauthorized} -> false
end
end

# We will start to authorize public endpoint on a next PR
defp authorized?(_conn, _type), do: true
end
23 changes: 23 additions & 0 deletions apps/rest_api/lib/ports/authorizer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule RestAPI.Ports.Authorizer do
@moduledoc """
Port to access Authorizer domain commands.
"""

alias Plug.Conn

@typedoc "All possible authorization responses"
@type possible_authorize_response :: :ok | {:error, :unauthorized}

@doc "Delegates to Authorizer.authorize_admin/1"
@callback authorize_admin(conn :: Conn.t()) :: possible_authorize_response()

@doc "Authorizes the subject using admin rule"
@spec authorize_admin(conn :: Conn.t()) :: possible_authorize_response()
def authorize_admin(conn), do: implementation().authorize_admin(conn)

defp implementation do
:rest_api
|> Application.get_env(__MODULE__)
|> Keyword.get(:domain)
end
end
7 changes: 6 additions & 1 deletion apps/rest_api/lib/routers/public.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule RestAPI.Routers.Public do
use RestAPI.Router

alias RestAPI.Controllers.Public
alias RestAPI.Plugs.Authentication
alias RestAPI.Plugs.{Authentication, Authorization}

pipeline :rest_api do
plug :accepts, ["json"]
Expand All @@ -14,6 +14,10 @@ defmodule RestAPI.Routers.Public do
plug Authentication
end

pipeline :authorized_by_admin do
plug Authorization, type: "admin"
end

scope "/api/v1", Public do
pipe_through :rest_api

Expand All @@ -31,6 +35,7 @@ defmodule RestAPI.Routers.Public do

scope "/admin/v1", RestAPI.Controller.Admin do
pipe_through :authenticated
pipe_through :authorized_by_admin

resources("/users", User, except: [:new])
end
Expand Down
1 change: 1 addition & 0 deletions apps/rest_api/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ defmodule RestAPI.MixProject do
# Umbrealla
{:resource_manager, in_umbrella: true},
{:authenticator, in_umbrella: true},
{:authorizer, in_umbrella: true},

# Domain
{:phoenix, "~> 1.5.4"},
Expand Down
8 changes: 7 additions & 1 deletion apps/rest_api/test/controllers/admin/user_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule RestAPI.Controllers.Admin.User do
use RestAPI.ConnCase, async: true

alias ResourceManager.Identities.Commands.Inputs.CreateUser
alias RestAPI.Ports.{AuthenticatorMock, ResourceManagerMock}
alias RestAPI.Ports.{AuthenticatorMock, AuthorizerMock, ResourceManagerMock}

@create_endpoint "/admin/v1/users"

Expand Down Expand Up @@ -63,6 +63,8 @@ defmodule RestAPI.Controllers.Admin.User do
}}
end)

expect(AuthorizerMock, :authorize_admin, fn %Plug.Conn{} -> :ok end)

assert %{
"id" => _id,
"inserted_at" => _inserted_at,
Expand Down Expand Up @@ -106,6 +108,8 @@ defmodule RestAPI.Controllers.Admin.User do
CreateUser.cast_and_apply(input)
end)

expect(AuthorizerMock, :authorize_admin, fn %Plug.Conn{} -> :ok end)

assert %{
"detail" => "The given params failed in validation",
"error" => "bad_request",
Expand Down Expand Up @@ -144,6 +148,8 @@ defmodule RestAPI.Controllers.Admin.User do
false
end)

expect(AuthorizerMock, :authorize_admin, fn %Plug.Conn{} -> :ok end)

assert %{
"detail" => "The given params failed in validation",
"error" => "bad_request",
Expand Down
80 changes: 80 additions & 0 deletions apps/rest_api/test/plugs/authorization_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
defmodule RestAPI.Plugs.AuthorizationTest do
use RestAPI.ConnCase, async: true

alias RestAPI.Plugs.Authorization
alias RestAPI.Ports.AuthorizerMock

describe "#{Authorization}.init/1" do
test "returns the given conn" do
assert [] == Authorization.init([])
end
end

describe "#{Authorization}.call/2" do
setup do
claims = default_claims()
{:ok, session: success_session(claims)}
end

test "succeeds and authorizer the subject in public endpoint", ctx do
conn = %{ctx.conn | private: %{session: ctx.session}}
assert %Plug.Conn{private: %{session: _}} = Authorization.call(conn, type: "public")
end

test "succeeds and authorizer the subject in admin endpoint", ctx do
conn = %{ctx.conn | private: %{session: ctx.session}}

expect(AuthorizerMock, :authorize_admin, fn _conn -> :ok end)

assert %Plug.Conn{private: %{session: _}} = Authorization.call(conn, type: "admin")
end

test "succeeds and authorizer the subject as public if option not passed", ctx do
conn = %{ctx.conn | private: %{session: ctx.session}}
assert %Plug.Conn{private: %{session: _}} = Authorization.call(conn, [])
end

test "fails if session not authenticated", %{conn: conn} do
assert %Plug.Conn{status: 401} = Authorization.call(conn, type: "admin")
end

test "fails if subject unauthorized", ctx do
conn = %{ctx.conn | private: %{session: ctx.session}}

expect(AuthorizerMock, :authorize_admin, fn _conn -> {:error, :unauthorized} end)

assert %Plug.Conn{status: 401} = Authorization.call(conn, type: "admin")
end
end

defp default_claims do
%{
"jti" => "03eds74a-c291-4b5f",
"aud" => "02eff74a-c291-4b5f-a02f-4f92d8daf693",
"azp" => "my-application",
"sub" => "272459ce-7356-4460-b461-1ecf0ebf7c4e",
"typ" => "Bearer",
"identity" => "user",
"scope" => "admin:read"
}
end

defp success_session(claims) do
%{
id: "02eff44a-c291-4b5f-a02f-4f92d8dbf693",
jti: claims["jti"],
subject_id: claims["sub"],
subject_type: claims["identity"],
expires_at: claims["expires_at"],
scopes: parse_scopes(claims["scope"]),
azp: claims["azp"],
claims: claims
}
end

defp parse_scopes(scope) when is_binary(scope) do
scope
|> String.split(" ", trim: true)
|> Enum.map(& &1)
end
end
3 changes: 3 additions & 0 deletions apps/rest_api/test/support/mocks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ for module <- [
# Authenticator domain
RestAPI.Ports.Authenticator,

# Authorizer domain
RestAPI.Ports.Authorizer,

# ResourceManager domain
RestAPI.Ports.ResourceManager
] do
Expand Down
1 change: 1 addition & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ config :rest_api, RestAPI.Endpoint,

config :rest_api, RestAPI.Application, children: [RestAPI.Telemetry, RestAPI.Endpoint]
config :rest_api, RestAPI.Ports.Authenticator, domain: Authenticator
config :rest_api, RestAPI.Ports.Authorizer, domain: Authorizer
config :rest_api, RestAPI.Ports.ResourceManager, domain: ResourceManager

import_config "#{Mix.env()}.exs"
1 change: 1 addition & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ config :rest_api, RestAPI.Endpoint,
server: false

config :rest_api, RestAPI.Ports.Authenticator, domain: RestAPI.Ports.AuthenticatorMock
config :rest_api, RestAPI.Ports.Authorizer, domain: RestAPI.Ports.AuthorizerMock
config :rest_api, RestAPI.Ports.ResourceManager, domain: RestAPI.Ports.ResourceManagerMock