Skip to content

Commit acaf189

Browse files
authored
feat: add public authorization (#64)
* feat: add public authorization * chore: remove unecessary module * feat: add public authorization * chore: remove unused controller * chore: add more tests
1 parent 6f99a1d commit acaf189

File tree

10 files changed

+136
-11
lines changed

10 files changed

+136
-11
lines changed

apps/authorizer/lib/authorizer.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ defmodule Authorizer do
33
Application to deal with request's to authorization server.
44
"""
55

6-
alias Authorizer.Rules.Commands.AdminAccess
6+
alias Authorizer.Rules.Commands.{AdminAccess, PublicAccess}
77

88
@doc "Delegates to #{AdminAccess}.execute/1"
99
defdelegate authorize_admin(conn), to: AdminAccess, as: :execute
10+
11+
@doc "Delegates to #{PublicAccess}.execute/1"
12+
defdelegate authorize_public(conn), to: PublicAccess, as: :execute
1013
end

apps/authorizer/lib/rules/commands/admin_access.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule Authorizer.Rules.Commands.AdminAccess do
22
@moduledoc """
33
Rule for authorizing a subject to do any action on admin endpoints.
44
5-
In order to authorize we have to execute verify if the subject matches some
5+
In order to authorize we have to execute an verification if the subject matches some
66
requirements as:
77
- It has admin flag enabled;
88
- It is status is active;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
defmodule Authorizer.Rules.Commands.PublicAccess do
2+
@moduledoc """
3+
Rule for authorizing a subject to do any action on public endpoints.
4+
5+
In order to authorize we have to execute an verification if the subject matches some
6+
requirements as:
7+
- It is status is active;
8+
"""
9+
10+
require Logger
11+
12+
alias Authorizer.Policies.SubjectActive
13+
alias Plug.Conn
14+
15+
@steps [SubjectActive]
16+
17+
@doc """
18+
Run the authorization flow in order to verify if the subject matches all requirements.
19+
This will call the following policies:
20+
- #{SubjectActive};
21+
"""
22+
@spec execute(conn :: Conn.t()) :: :ok | {:error, :unauthorized}
23+
def execute(%Conn{} = conn) do
24+
@steps
25+
|> Enum.reduce_while([], fn policy, opts -> run_policy(policy, conn, opts) end)
26+
|> case do
27+
{:error, :unauthorized} ->
28+
Logger.error("Failed on some of the policies")
29+
{:error, :unauthorized}
30+
31+
_success ->
32+
:ok
33+
end
34+
end
35+
36+
defp run_policy(policy, conn, opts) do
37+
with {:ok, context} <- policy.validate(conn),
38+
{:ok, shared_context} <- policy.execute(context, opts) do
39+
{:cont, shared_context}
40+
else
41+
error -> {:halt, error}
42+
end
43+
end
44+
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
defmodule Authorizer.Rules.Commands.PublicAccessTest do
2+
use Authorizer.DataCase, async: true
3+
4+
alias Authorizer.Ports.ResourceManagerMock
5+
alias Authorizer.Rules.Commands.PublicAccess
6+
7+
setup do
8+
conn = %Plug.Conn{
9+
private: %{
10+
session: %{
11+
id: Ecto.UUID.generate(),
12+
jti: Ecto.UUID.generate(),
13+
subject_id: Ecto.UUID.generate(),
14+
subject_type: "user",
15+
expires_at: NaiveDateTime.add(NaiveDateTime.utc_now(), 10_000),
16+
scopes: ["admin:read", "admin.write"],
17+
azp: "Watcher Ex"
18+
}
19+
}
20+
}
21+
22+
{:ok, conn: conn}
23+
end
24+
25+
describe "#{PublicAccess}.execute/1" do
26+
test "succeeds if subject is active and is an admin", %{conn: conn} do
27+
expect(ResourceManagerMock, :get_identity, fn %{id: user_id} ->
28+
assert conn.private.session.subject_id == user_id
29+
{:ok, %{status: "active", is_admin: true}}
30+
end)
31+
32+
assert :ok == PublicAccess.execute(conn)
33+
end
34+
35+
test "fails if identity is not active", %{conn: conn} do
36+
expect(ResourceManagerMock, :get_identity, fn %{id: user_id} ->
37+
assert conn.private.session.subject_id == user_id
38+
{:ok, %{status: "blocked", is_admin: true}}
39+
end)
40+
41+
assert {:error, :unauthorized} == PublicAccess.execute(conn)
42+
end
43+
44+
test "fails if identity was not found", %{conn: conn} do
45+
expect(ResourceManagerMock, :get_identity, fn %{id: user_id} ->
46+
assert conn.private.session.subject_id == user_id
47+
{:error, :not_found}
48+
end)
49+
50+
assert {:error, :unauthorized} == PublicAccess.execute(conn)
51+
end
52+
end
53+
end

apps/rest_api/lib/controllers/public/auth.ex

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ defmodule RestAPI.Controllers.Public.Auth do
1111
}
1212

1313
alias RestAPI.Ports.Authenticator, as: Commands
14-
alias RestAPI.Views.Public.SignIn
14+
alias RestAPI.Views.Public.Auth
1515

1616
action_fallback RestAPI.Controllers.Fallback
1717

@@ -30,7 +30,7 @@ defmodule RestAPI.Controllers.Public.Auth do
3030
with {:ok, input} <- ResourceOwner.cast_and_apply(params),
3131
{:ok, response} <- Commands.sign_in_resource_owner(input) do
3232
conn
33-
|> put_view(SignIn)
33+
|> put_view(Auth)
3434
|> put_status(200)
3535
|> render("token.json", response: response)
3636
end
@@ -42,7 +42,7 @@ defmodule RestAPI.Controllers.Public.Auth do
4242
with {:ok, input} <- RefreshToken.cast_and_apply(params),
4343
{:ok, response} <- Commands.sign_in_refresh_token(input) do
4444
conn
45-
|> put_view(SignIn)
45+
|> put_view(Auth)
4646
|> put_status(200)
4747
|> render("token.json", response: response)
4848
end
@@ -54,7 +54,7 @@ defmodule RestAPI.Controllers.Public.Auth do
5454
with {:ok, input} <- ClientCredentials.cast_and_apply(params),
5555
{:ok, response} <- Commands.sign_in_client_credentials(input) do
5656
conn
57-
|> put_view(SignIn)
57+
|> put_view(Auth)
5858
|> put_status(200)
5959
|> render("token.json", response: response)
6060
end
@@ -66,7 +66,7 @@ defmodule RestAPI.Controllers.Public.Auth do
6666
with {:ok, input} <- AuthorizationCode.cast_and_apply(params),
6767
{:ok, response} <- Commands.sign_in_authorization_code(input) do
6868
conn
69-
|> put_view(SignIn)
69+
|> put_view(Auth)
7070
|> put_status(200)
7171
|> render("token.json", response: response)
7272
end

apps/rest_api/lib/plugs/authorization.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ defmodule RestAPI.Plugs.Authorization do
4141
end
4242
end
4343

44+
defp authorized?(conn, "public") do
45+
conn
46+
|> Authorizer.authorize_public()
47+
|> case do
48+
:ok -> true
49+
{:error, :unauthorized} -> false
50+
end
51+
end
52+
4453
# We will start to authorize public endpoint on a next PR
4554
defp authorized?(_conn, _type), do: true
4655
end

apps/rest_api/lib/ports/authorizer.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,17 @@ defmodule RestAPI.Ports.Authorizer do
1111
@doc "Delegates to Authorizer.authorize_admin/1"
1212
@callback authorize_admin(conn :: Conn.t()) :: possible_authorize_response()
1313

14+
@doc "Delegates to Authorizer.authorize_public/1"
15+
@callback authorize_public(conn :: Conn.t()) :: possible_authorize_response()
16+
1417
@doc "Authorizes the subject using admin rule"
1518
@spec authorize_admin(conn :: Conn.t()) :: possible_authorize_response()
1619
def authorize_admin(conn), do: implementation().authorize_admin(conn)
1720

21+
@doc "Authorizes the subject using public rule"
22+
@spec authorize_public(conn :: Conn.t()) :: possible_authorize_response()
23+
def authorize_public(conn), do: implementation().authorize_public(conn)
24+
1825
defp implementation do
1926
:rest_api
2027
|> Application.get_env(__MODULE__)

apps/rest_api/lib/routers/admin.ex

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@ defmodule RestAPI.Routers.Admin do
1010
plug Tracker
1111
end
1212

13-
pipeline :authorized_by_admin do
13+
pipeline :authorized_as_user do
14+
plug Authorization, type: "public"
15+
end
16+
17+
pipeline :authorized_as_admin do
1418
plug Authorization, type: "admin"
1519
end
1620

1721
scope "/v1", RestAPI.Controller.Admin do
1822
pipe_through :authenticated
19-
pipe_through :authorized_by_admin
23+
pipe_through :authorized_as_admin
2024

21-
resources("/users", User, except: [:new])
25+
resources "/users", User, except: [:new]
2226
end
2327
end

apps/rest_api/lib/views/public/sign_in.ex renamed to apps/rest_api/lib/views/public/auth.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
defmodule RestAPI.Views.Public.SignIn do
1+
defmodule RestAPI.Views.Public.Auth do
22
@moduledoc false
33

44
use RestAPI.View

apps/rest_api/test/plugs/authorization_test.exs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ defmodule RestAPI.Plugs.AuthorizationTest do
1818

1919
test "succeeds and authorizer the subject in public endpoint", ctx do
2020
conn = %{ctx.conn | private: %{session: ctx.session}}
21+
22+
expect(AuthorizerMock, :authorize_public, fn _conn -> :ok end)
23+
2124
assert %Plug.Conn{private: %{session: _}} = Authorization.call(conn, type: "public")
2225
end
2326

@@ -42,8 +45,10 @@ defmodule RestAPI.Plugs.AuthorizationTest do
4245
conn = %{ctx.conn | private: %{session: ctx.session}}
4346

4447
expect(AuthorizerMock, :authorize_admin, fn _conn -> {:error, :unauthorized} end)
48+
expect(AuthorizerMock, :authorize_public, fn _conn -> {:error, :unauthorized} end)
4549

4650
assert %Plug.Conn{status: 401} = Authorization.call(conn, type: "admin")
51+
assert %Plug.Conn{status: 401} = Authorization.call(conn, type: "public")
4752
end
4853
end
4954

0 commit comments

Comments
 (0)