Skip to content

Instantly share code, notes, and snippets.

@ashrafhasson
Last active April 21, 2016 02:49
Show Gist options
  • Save ashrafhasson/56f6b72fcb75504278f5aee834456f49 to your computer and use it in GitHub Desktop.
Save ashrafhasson/56f6b72fcb75504278f5aee834456f49 to your computer and use it in GitHub Desktop.
Guardian modification to allow OR behaviour when checking permissions
defmodule Guardian.Plug.EnsurePermissions do
@moduledoc """
Use this plug to ensure that there are the
correct permissions set in the claims found on the connection.
### Example
alias Guardian.Plug.EnsurePermissions
# read and write permissions for the admin set
plug EnsurePermissions, admin: [:read, :write], handler: SomeMod,
# read AND write permissions for the admin set
# AND :profile for the default set
plug EnsurePermissions, admin: [:read, :write],
default: [:profile],
handler: SomeMod
# read AND write permissions for the admin set
# OR :profile for the default set
plug EnsurePermissions, one_of: %{ admin: [:read, :write],
default: [:profile] },
handler: SomeMod
# admin :read AND :write for the claims located in the :secret location
plug EnsurePermissions, key: :secret,
admin: [:read, :write],
handler:SomeMod
On failure will be handed the connection with the conn,
and params where reason: `:forbidden`
The handler will be called on failure.
The `:unauthorized` function will be called when a failure is detected.
"""
require Logger
import Plug.Conn
def init(opts) do
opts = Enum.into(opts, %{})
on_failure = Map.get(opts, :on_failure)
key = Map.get(opts, :key, :default)
handler = Map.get(opts, :handler)
one_of = Map.get(opts, :one_of)
perms = Map.drop(opts, [:handler, :on_failure, :key, :one_of])
if handler do
handler = {handler, :unauthorized}
else
handler = case on_failure do
{mod, f} ->
Logger.log(:warn, "on_failure is deprecated. Use handler")
{mod, f}
_ -> raise "Requires a handler module to be passed"
end
end
%{
handler: handler,
key: key,
perm_keys: Map.keys(perms),
one_of: one_of,
perms: perms,
}
end
@doc false
def call(conn, opts) do
key = Map.get(opts, :key)
case Guardian.Plug.claims(conn, key) do
{:ok, claims} ->
perms = Map.get(opts, :perms, %{})
result =
case Map.get(opts, :one_of) do
nil -> check_all_perm_sets(Map.get(opts, :perm_keys), claims, perms)
one_of -> check_any_perm_set(Map.keys(one_of), claims, one_of)
end
if result, do: conn, else: handle_error(conn, opts)
{:error, _} -> handle_error(conn, opts)
end
end
defp check_any_perm_set(perm_keys, claims, perms) do
Enum.any?(perm_keys, fn(perm_key) ->
found_perms = Guardian.Permissions.from_claims(claims, perm_key)
check_permissions(found_perms, Map.get(perms, perm_key), perm_key)
end)
end
defp check_all_perm_sets(perm_keys, claims, perms) do
Enum.all?(perm_keys, fn(perm_key) ->
found_perms = Guardian.Permissions.from_claims(claims, perm_key)
check_permissions(found_perms, Map.get(perms, perm_key), perm_key)
end)
end
defp check_permissions(found_perms, perms, perm_key) do
Guardian.Permissions.all?(
found_perms,
perms,
perm_key
)
end
defp handle_error(conn, opts) do
the_connection = conn |> assign(:guardian_failure, :forbidden) |> halt
{mod, meth} = Map.get(opts, :handler)
apply(
mod,
meth,
[
the_connection,
Map.merge(the_connection.params, %{reason: :forbidden})
]
)
end
end
defmodule Guardian.Plug.EnsurePermissionTest do
@moduledoc false
use ExUnit.Case, async: true
use Plug.Test
import Guardian.TestHelper
alias Guardian.Plug.EnsurePermissions
defmodule TestHandler do
@moduledoc false
def unauthorized(conn, _) do
conn
|> Plug.Conn.assign(:guardian_spec, :forbidden)
|> Plug.Conn.send_resp(401, "Unauthorized")
end
end
setup do
conn = conn(:get, "/foo")
{:ok, %{conn: conn}}
end
test "doesnt call unauthorized when permissions are present", %{conn: conn} do
pems = Guardian.Permissions.to_value([:read, :write])
claims = %{"pem" => %{"default" => pems}}
expected_conn =
conn
|> Guardian.Plug.set_claims({:ok, claims})
|> Plug.Conn.fetch_query_params
|> run_plug(EnsurePermissions, handler: TestHandler,
default: [:read, :write])
refute unauthorized?(expected_conn)
end
test "is invalid when missing a requested permission", %{conn: conn} do
pems = Guardian.Permissions.to_value([:read])
claims = %{"pem" => %{"default" => pems}}
expected_conn =
conn
|> Guardian.Plug.set_claims({:ok, claims})
|> Plug.Conn.fetch_query_params
|> run_plug(EnsurePermissions, handler: TestHandler,
default: [:read, :write])
assert unauthorized?(expected_conn)
end
test "is invalid when claims don't include the pem key", %{conn: conn} do
pems = Guardian.Permissions.to_value([:other_read], :other)
claims = %{"pem" => %{"default" => pems}}
expected_conn =
conn
|> Guardian.Plug.set_claims({:ok, claims})
|> Plug.Conn.fetch_query_params
|> run_plug(EnsurePermissions, handler: TestHandler,
default: [:read, :write])
assert unauthorized?(expected_conn)
end
test "is invalid when all permissions are not present", %{conn: conn} do
pems = Guardian.Permissions.to_value(
[:read, :write, :update, :delete],
:default
)
other_pems = Guardian.Permissions.to_value([:other_write], :other)
claims = %{"pem" => %{"default" => pems, "other" => other_pems}}
expected_conn =
conn
|> Guardian.Plug.set_claims({:ok, claims})
|> Plug.Conn.fetch_query_params
|> run_plug(EnsurePermissions, handler: TestHandler,
default: [:read, :write], other: [:other_read])
assert unauthorized?(expected_conn)
end
test "is valid when all permissions are present", %{conn: conn} do
pems = Guardian.Permissions.to_value(
[:read, :write, :update, :delete],
:default
)
other_pems = Guardian.Permissions.to_value(
[:other_read, :other_write],
:other
)
claims = %{"pem" => %{"default" => pems, "other" => other_pems}}
expected_conn =
conn
|> Guardian.Plug.set_claims({:ok, claims})
|> Plug.Conn.fetch_query_params
|> run_plug(EnsurePermissions, handler: TestHandler,
default: [:read, :write], other: [:other_read])
refute unauthorized?(expected_conn)
end
test "is invalid when non of the one_of permissions set is present", %{conn: conn} do
pems = Guardian.Permissions.to_value(
[:read, :write, :update, :delete],
:default
)
other_pems = Guardian.Permissions.to_value(
[:other_read, :other_write],
:other
)
claims = %{"pem" => %{"admin" => pems, "special" => other_pems}}
expected_conn =
conn
|> Guardian.Plug.set_claims({:ok, claims})
|> Plug.Conn.fetch_query_params
|> run_plug(EnsurePermissions, handler: TestHandler,
one_of: %{other: [:other_read], default: [:read, :write]})
assert unauthorized?(expected_conn)
end
test "is valid when at least one_of the permissions set is present", %{conn: conn} do
pems = Guardian.Permissions.to_value(
[:read, :write, :update, :delete],
:default
)
other_pems = Guardian.Permissions.to_value(
[:other_read, :other_write],
:other
)
claims = %{"pem" => %{"special" => other_pems, "default" => pems}}
expected_conn =
conn
|> Guardian.Plug.set_claims({:ok, claims})
|> Plug.Conn.fetch_query_params
|> run_plug(EnsurePermissions, handler: TestHandler,
one_of: %{other: [:other_read], default: [:read, :write]})
refute unauthorized?(expected_conn)
end
test "halts the connection", %{conn: conn} do
pems = Guardian.Permissions.to_value(
[:read, :write, :update, :delete],
:default
)
other_pems = Guardian.Permissions.to_value([:other_write], :other)
claims = %{"pem" => %{"default" => pems, "other" => other_pems}}
expected_conn =
conn
|> Guardian.Plug.set_claims({:ok, claims})
|> Plug.Conn.fetch_query_params
|> run_plug(EnsurePermissions, handler: TestHandler,
default: [:read, :write], other: [:other_read])
assert expected_conn.halted
end
def unauthorized?(conn) do
conn.assigns[:guardian_spec] == :forbidden
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment