Current section
Files
Jump to
Current section
Files
lib/rbac.ex
defmodule RBAC do
@moduledoc """
Documentation for `Rbac`.
"""
@doc """
Transform a list of maps (roles) to comma-separated string of ids.
## Examples
iex> RBAC.transform_role_list_to_string([%{id: 1}, %{id: 2}, %{id: 3}])
"1,2,3"
iex> RBAC.transform_role_list_to_string("1,2,3")
"1,2,3"
iex> RBAC.transform_role_list_to_string(%{name: "sub", id: 1, revoked: nil})
"1"
iex> RBAC.transform_role_list_to_string([%{id: 1, revoked: 1}, %{id: 3}])
"3"
"""
def transform_role_list_to_string(roles) when is_list(roles) do
# remove any roles that have been revoked:
Enum.filter(roles, fn role ->
not Map.has_key?(role, :revoked) or is_nil(role.revoked)
end)
|> Enum.map_join(",", & &1.id)
end
# this guard is meant to return the string form if it's already defined:
def transform_role_list_to_string(roles) when is_binary(roles) do
roles
end
# if roles is a struct/map then attempt to parse it:
def transform_role_list_to_string(roles) do
[Map.delete(roles, :__meta__)] |> transform_role_list_to_string()
end
@doc """
`get_approles/2` fetches the roles for the app
"""
def get_approles(auth_url, client_id) do
url = "#{auth_url}/approles/#{client_id}"
HTTPoison.start()
HTTPoison.get(url)
|> parse_body_response()
end
@doc """
`parse_body_response/1` parses the response
so your app can use the resulting JSON (list of roles).
"""
@spec parse_body_response({atom, String.t()}) :: String.t()
def parse_body_response({:error, err}), do: {:error, err}
def parse_body_response({:ok, response}) do
body = Map.get(response, :body)
# IO.inspect(body)
# make keys of map atoms for easier access in templates
if body == nil do
{:error, :no_body}
else
{:ok, str_key_map} = Jason.decode(body)
atom_key_map =
Enum.map(str_key_map, fn role ->
for {key, val} <- role, into: %{}, do: {String.to_atom(key), val}
end)
{:ok, atom_key_map}
end
# https://stackoverflow.com/questions/31990134
end
@doc """
`init_roles/2 fetches the list of roles for an app
from the auth app (auth_url) based on the client_id
and caches the list for fast access.
ETS is an in-memory cache you get for *Free* in Elixir/Erlang.
See: https://elixir-lang.org/getting-started/mix-otp/ets.html
and: https://elixirschool.com/en/lessons/specifics/ets
"""
def init_roles_cache(auth_url, client_id) do
{:ok, roles} = RBAC.get_approles(auth_url, client_id)
:ets.new(:roles_cache, [:set, :protected, :named_table])
# insert full list:
:ets.insert(:roles_cache, {"roles", roles})
# insert individual roles for fast lookup:
Enum.each(roles, fn role ->
:ets.insert(:roles_cache, {role.name, role})
:ets.insert(:roles_cache, {role.id, role})
end)
end
@doc """
`get_role_from_cache/1 retrieves a role from ets cache
"""
def get_role_from_cache(term) do
case :ets.lookup(:roles_cache, term) do
# not found:
[] -> %{id: 0}
# extract role:
[{_term, role}] -> role
end
end
@doc """
`has_role/2 confirms if the person has the given role
e.g:
has_role(conn, "home_admin") > true
has_role(conn, "potus") > false
"""
def has_role(conn, role_name) do
role = get_role_from_cache(role_name)
person_roles =
conn.assigns.person.roles
|> String.split(",", trim: true)
|> Enum.map(&String.to_integer/1)
Enum.member?(person_roles, role.id)
end
end