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
4 changes: 4 additions & 0 deletions apps/authorizer/.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}"]
]
24 changes: 24 additions & 0 deletions apps/authorizer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 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").
authorizer-*.tar

21 changes: 21 additions & 0 deletions apps/authorizer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Authorizer

**TODO: Add description**

## Installation

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

```elixir
def deps do
[
{:authorizer, "~> 0.1.0"}
]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/authorizer](https://hexdocs.pm/authorizer).

10 changes: 10 additions & 0 deletions apps/authorizer/lib/authorizer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Authorizer do
@moduledoc """
Application to deal with request's to authorization server.
"""

alias Authorizer.Rules.Commands.AdminAccess

@doc "Delegates to #{AdminAccess}.execute/1"
defdelegate authorize_admin(conn), to: AdminAccess, as: :execute
end
69 changes: 69 additions & 0 deletions apps/authorizer/lib/policies/admin_allowed.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
defmodule Authorizer.Policies.AdminAllowed do
@moduledoc """
Authorization policy to ensure that an subject is an admin.
"""

require Logger

alias Authorizer.Ports.ResourceManager
alias Plug.Conn

@behaviour Authorizer.Policies.Behaviour

@subject_types ~w(user application)

@impl true
def info do
"""
Ensures that a specific subject is allowed to do an admin action.
In order to succeed it has to have `is_admin` set as `true`.
"""
end

@impl true
def validate(%Conn{private: %{session: session}} = context) when is_map(session) do
case session do
%{subject_id: id, subject_type: type} when is_binary(id) and type in @subject_types ->
Logger.debug("Policity #{__MODULE__} validated with success")
{:ok, context}

_any ->
Logger.error("Policy #{__MODULE__} failed on validation because session is invalid")
{:error, :unauthorized}
end
end

def validate(%Conn{private: %{session: _}}) do
Logger.error("Policy #{__MODULE__} failed on validation because session was not found")
{:error, :unauthorized}
end

@impl true
def execute(%Conn{private: %{session: session}}, opts \\ [])
when is_map(session) and is_list(opts) do
# We look for the identity on shared context first
identity = Keyword.get(opts, :identity)

with {:identity, {:ok, identity}} <- {:identity, get_identity(identity || session)},
{:admin?, true} <- {:admin?, identity.is_admin} do
Logger.debug("Policy #{__MODULE__} execution succeeded")
{:ok, Keyword.put(opts, :identity, identity)}
else
{:identity, error} ->
Logger.error("Policy #{__MODULE__} failed to get identity", error: inspect(error))
{:error, :unauthorized}

{:admin?, false} ->
Logger.error("Policy #{__MODULE__} failed because subject is not an admin")
{:error, :unauthorized}
end
end

defp get_identity(%{subject_id: subject_id, subject_type: "user"}),
do: ResourceManager.get_identity(%{id: subject_id, username: nil})

defp get_identity(%{subject_id: subject_id, subject_type: "application"}),
do: ResourceManager.get_identity(%{id: subject_id, client_id: nil})

defp get_identity(%{status: _} = identity), do: {:ok, identity}
end
15 changes: 15 additions & 0 deletions apps/authorizer/lib/policies/behaviour.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Authorizer.Policies.Behaviour do
@moduledoc """
A policy is a set of verifications to make sure that a subject can do such action.
"""

@doc "Return the policy description"
@callback info() :: String.t()

@doc "Runs the input validations"
@callback validate(conn :: Plug.Conn.t()) :: {:ok, context :: map()} | {:error, atom()}

@doc "Runs the authorization policy"
@callback execute(context :: map(), opts :: Keyword.t()) ::
{:ok, shared_context :: Keyword.t()} | {:error, :unauthorized}
end
69 changes: 69 additions & 0 deletions apps/authorizer/lib/policies/subject_active.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
defmodule Authorizer.Policies.SubjectActive do
@moduledoc """
Authorization policy to ensure that an subject is active.
"""

require Logger

alias Authorizer.Ports.ResourceManager
alias Plug.Conn

@behaviour Authorizer.Policies.Behaviour

@subject_types ~w(user application)

@impl true
def info do
"""
Ensures that a specific subject is active.
In order to succeed it has to have `status` set as `active`.
"""
end

@impl true
def validate(%Conn{private: %{session: session}} = context) when is_map(session) do
case session do
%{subject_id: id, subject_type: type} when is_binary(id) and type in @subject_types ->
Logger.debug("Policity #{__MODULE__} validated with success")
{:ok, context}

_any ->
Logger.error("Policy #{__MODULE__} failed on validation because session is invalid")
{:error, :unauthorized}
end
end

def validate(%Conn{private: %{session: _}}) do
Logger.error("Policy #{__MODULE__} failed on validation because session was not found")
{:error, :unauthorized}
end

@impl true
def execute(%Conn{private: %{session: session}}, opts \\ [])
when is_map(session) and is_list(opts) do
# We look for the identity on shared context first
identity = Keyword.get(opts, :identity)

with {:identity, {:ok, identity}} <- {:identity, get_identity(identity || session)},
{:active?, "active"} <- {:active?, identity.status} do
Logger.debug("Policy #{__MODULE__} execution succeeded")
{:ok, Keyword.put(opts, :identity, identity)}
else
{:identity, error} ->
Logger.error("Policy #{__MODULE__} failed to get identity", error: inspect(error))
{:error, :unauthorized}

{:active?, status} ->
Logger.error("Policy #{__MODULE__} failed because subject is status is #{status}")
{:error, :unauthorized}
end
end

defp get_identity(%{subject_id: subject_id, subject_type: "user"}),
do: ResourceManager.get_identity(%{id: subject_id, username: nil})

defp get_identity(%{subject_id: subject_id, subject_type: "application"}),
do: ResourceManager.get_identity(%{id: subject_id, client_id: nil})

defp get_identity(%{status: _} = identity), do: {:ok, identity}
end
21 changes: 21 additions & 0 deletions apps/authorizer/lib/ports/resource_manager.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule Authorizer.Ports.ResourceManager do
@moduledoc """
Port to access ResourceManager domain commands.
"""

@typedoc "All possible responses"
@type possible_responses :: {:ok, identity :: struct()} | {:error, :not_found | :invalid_params}

@doc "Delegates to ResourceManager.get_identity/1"
@callback get_identity(input :: map()) :: possible_responses()

@doc "Gets a subject identity by the given input"
@spec get_identity(input :: map()) :: possible_responses()
def get_identity(input), do: implementation().get_identity(input)

defp implementation do
:authorizer
|> Application.get_env(__MODULE__)
|> Keyword.get(:domain)
end
end
49 changes: 49 additions & 0 deletions apps/authorizer/lib/rules/commands/admin_access.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule Authorizer.Rules.Commands.AdminAccess do
@moduledoc """
Rule for authorizing a subject to do any action on admin endpoints.

In order to authorize we have to execute verify if the subject matches some
requirements as:
- It has admin flag enabled;
- It is status is active;
"""

require Logger

alias Authorizer.Policies.{AdminAllowed, SubjectActive}
alias Plug.Conn

@steps [
SubjectActive,
AdminAllowed
]

@doc """
Run the authorization flow in order to verify if the subject matches all requirements.
This will call the following policies:
- #{SubjectActive};
- #{AdminAllowed};
"""
@spec execute(conn :: Conn.t()) :: :ok | {:error, :unauthorized}
def execute(%Conn{} = conn) do
@steps
|> Enum.reduce_while([], fn policy, opts -> run_policy(policy, conn, opts) end)
|> case do
{:error, :unauthorized} ->
Logger.error("Failed on some of the policies")
{:error, :unauthorized}

_success ->
:ok
end
end

defp run_policy(policy, conn, opts) do
with {:ok, context} <- policy.validate(conn),
{:ok, shared_context} <- policy.execute(context, opts) do
{:cont, shared_context}
else
error -> {:halt, error}
end
end
end
49 changes: 49 additions & 0 deletions apps/authorizer/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule Authorizer.MixProject do
use Mix.Project

def project do
[
app: :authorizer,
version: "0.1.0",
build_path: "../../_build",
config_path: "../../config/config.exs",
elixirc_paths: elixirc_paths(Mix.env()),
deps_path: "../../deps",
lockfile: "../../mix.lock",
elixir: "~> 1.11",
start_permanent: Mix.env() == :prod,
deps: deps(),
test_coverage: [tool: ExCoveralls]
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

# This makes sure the factory and any other modules in test/support are compiled
# when in the test environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]

defp deps do
[
# Umbrella
{:resource_manager, in_umbrella: true},

# Domain
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"},

# Tools
{:dialyxir, "~> 1.0", only: :dev, runtime: false},
{:credo, "~> 1.4", only: [:dev, :test], runtime: false},
{:ex_doc, "~> 0.22", only: :dev, runtime: false},
{:excoveralls, "~> 0.13", only: :test},
{:mox, "~> 0.5", only: :test}
]
end
end
Loading