diff --git a/.gitignore b/.gitignore
index 216cd02..5f0df53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,4 @@ rbac-*.tar
*.beam
/config/*.secret.exs
.elixir_ls/
+.env
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 4041f10..43d220b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,8 @@
language: elixir
elixir:
- - 1.10.2
+ - 1.10.4
otp_release:
- - 22.1.8
+ - 23.0.3
env:
- MIX_ENV=test
script:
diff --git a/README.md b/README.md
index 6629ae0..ed0941a 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
# `rbac`
-Role Based Access Control (RBAC) gives you
+Role Based Access Control (**`RBAC`**) gives you
a human-friendly way of controlling access
to specific data/features in your App(s).
@@ -20,7 +20,9 @@ to specific data/features in your App(s).
## Why?
-RBAC lets you easily manage roles and permissions in any application
+You want an _easy_ way to restrict access to features fo your Elixir/Phoenix App
+based on a sane model of roles.
+**`RBAC`** lets you _easily_ manage roles and permissions in any application
and see at a glance exactly which permissions a person has in the system.
It reduces complexity over traditional
Access Control List (ACL) based permissions systems.
@@ -29,7 +31,7 @@ Access Control List (ACL) based permissions systems.
## What?
-The purpose of RBAC is to provide a framework
+The purpose of **`RBAC`** is to provide a framework
for application administrators and developers
to manage the permissions assigned to the people using the App(s).
@@ -39,7 +41,7 @@ to manage the permissions assigned to the people using the App(s).
Anyone who is interested in developing secure applications
used by many people with differing needs and permissions
-should learn about RBAC.
+should learn about **`RBAC`**.
## _How_?
@@ -52,21 +54,253 @@ Install by adding `rbac` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
- {:rbac, "~> 0.1.0"}
+ {:rbac, "~> 0.5.0"}
]
end
```
-API/Function reference available at
-[https://hexdocs.pm/rbac](https://hexdocs.pm/rbac).
+
+
+### Initialize Your Roles List (Cache)
+
+In order use **`RBAC`** you need to initialize
+the _in-memory cache_ with a list of roles.
+
+#### Got your Own List of Roles?
+
+If you prefer to manage your own list of roles
+you can simply supply your own list of roles e.g:
+
+```elixir
+roles = [%{id: 1, name: "admin"}, %{id: 2, name: "subscriber"}]
+RBAC.insert_roles_into_ets_cache(roles)
+```
+
+To initialize the list of roles _once_ (_at boot_) for your Phoenix App,
+open the `application.ex` file of your project
+and locate the `def start(_type, _args) do` definition, e.g:
+
+```elixir
+def start(_type, _args) do
+ # List all child processes to be supervised
+ children = [
+ # Start the Ecto repository
+ App.Repo,
+ # Start the endpoint when the application starts
+ {Phoenix.PubSub, name: App.PubSub},
+ AppWeb.Endpoint
+ # Starts a worker by calling: Auth.Worker.start_link(arg)
+ # {Auth.Worker, arg},
+ ]
+
+ # See https://hexdocs.pm/elixir/Supervisor.html
+ # for other strategies and supported options
+ opts = [strategy: :one_for_one, name: App.Supervisor]
+ Supervisor.start_link(children, opts)
+end
+```
+
+Add the following code at the top of the `start/2` function definition:
+
+```elixir
+# initialize RBAC Roles Cache:
+roles = [%{id: 1, name: "admin"}, %{id: 2, name: "subscriber"}]
+RBAC.insert_roles_into_ets_cache(roles)
+```
+
+#### Using `auth` to Manage Roles?
+
+**`RBAC`** is _independent_ from our
+[`auth`](https://github.com/dwyl/auth) App
+and it's corresponding helper library
+[`auth_plug`](https://github.com/dwyl/auth_plug).
+
+However if you want a ready-made list of universally applicable roles
+and an _easy_ way to manage and create custom roles for your App,
+**`auth`** has you covered:
+https://dwylauth.herokuapp.com
+Once you have exported your
+`AUTH_API_KEY` Environment Variable
+following these instructions:
+https://github.com/dwyl/auth_plug#2-get-your-auth_api_key-
+You can source your list of roles
+and initalize it
+with the following code:
+
+```elixir
+# initialize RBAC Roles Cache:
+RBAC.init_roles_cache(
+ "https://dwylauth.herokuapp.com",
+ AuthPlug.Token.client_id()
+)
+```
+
+`AuthPlug.Token.client_id()`
+expects the `AUTH_API_KEY` Environment Variable to be set.
+
+
+
### Usage
+Once you have added the initialization code,
+you can easily check that a person has a required role
+using the following code:
+
+```elixir
+# role argument as String
+RBAC.has_role?([2], "admin")
+> true
+
+# role argument as Atom
+RBAC.has_role?([2], :admin)
+> true
+
+# second argument (role) as Integer
+RBAC.has_role?([2], 2)
+> true
+```
+
+The first argument is a `List` of role ids.
+The second argument (`role`) can either be
+an `String`, `Atom` or`Integer`
+corresponding to the `name` of the role
+or the `id` respectively.
+We prefer using `String` because its more developer/maintenance friendly.
+We can immediately see which role is required
+
+
+
+Or if you want to check that the person has _any_ role
+in a list of potential roles:
+
+```elixir
+RBAC.has_role_any?([2,4,7], ["admin", "commenter"])
+> true
+
+RBAC.has_role_any?([2,4,7], [:admin, :commenter])
+> true
+```
+
+
+### Using `rbac` with `auth_plug`
+
+If you are using [`auth_plug`](https://github.com/dwyl/auth_plug)
+to handle checking auth in your App.
+It adds the `person` map to the `conn.assigns` struct.
+That means the person's roles are listed in:
+`conn.assigns.person.roles`
+
+e.g:
+```elixir
+%{
+ app_id: 8,
+ auth_provider: "github",
+ email: "alex.mcawesome@gmail.com",
+ exp: 1631721211,
+ givenName: "Alex",
+ id: 772,
+ roles: "2"
+}
+```
+
+For convenience, we allow the first argument
+of both `has_role/2` and `has_role_any?/2`
+to accept `conn` as the first argument:
+
+```elixir
+RBAC.has_role?(conn, "admin")
+> true
+```
+
+Check that the person has has any role in a list of potential roles:
+
+```elixir
+RBAC.has_role_any?(conn, ["admin", "commenter"])
+> true
+```
+
+We prefer to make our code as declarative and human-friendly as possible,
+hence the `String` role names.
+However both the role-checking functions also accept a list of integers,
+corresponding to the `role.id` of the required role, e.g:
+
+```elixir
+RBAC.has_role?(conn, 2)
+> true
+```
+
+If the person does not have the **`superadmin`** role,
+`has_role?/2` will return `false`
+
+```elixir
+RBAC.has_role?(conn, 1)
+> false
+```
+
+Or supply a list of integers to `has_role_any?/2` if you prefer:
+
+```elixir
+RBAC.has_role_any?(conn, [1,2,3])
+> true
+```
+
+You can even _mix_ the type in the list (_though we don't recommend it..._):
+
+```elixir
+RBAC.has_role_any?(conn, ["admin",2,3])
+> true
+```
+
+We recommend picking one, and advise using strings for code legibility.
+e.g:
+
+```elixir
+RBAC.has_role?(conn, "building_admin")
+```
+
+Is very clear which role is required.
+Whereas using an `int` (_especially for custom roles_) is a bit more terse:
+
+```elixir
+RBAC.has_role?(conn, 13)
+```
+
+It requires the developer/code reviewer/maintainer
+to either know what the role is,
+or look it up in a list.
+Stick with `String` as your role names in your code.
+API/Function reference available at
+[https://hexdocs.pm/rbac](https://hexdocs.pm/rbac).
+
+
+
## tl;dr > RBAC Knowledge Summary
@@ -100,8 +334,6 @@ An operation can only be completed
if the person attempting to complete the transaction
possesses the appropriate role.
-
-
## Recommended Reading
+ https://en.wikipedia.org/wiki/Role-based_access_control
diff --git a/lib/rbac.ex b/lib/rbac.ex
index 3e183ce..0e4ca0a 100644
--- a/lib/rbac.ex
+++ b/lib/rbac.ex
@@ -2,9 +2,12 @@ defmodule RBAC do
@moduledoc """
Documentation for `Rbac`.
"""
+ require Logger
@doc """
- Transform a list of maps (roles) to comma-separated string of ids.
+ `transform_role_list_to_string/1` transforms a list of maps (roles)
+ to comma-separated string of ids (minimal data use)
+ which is JSON-compatible and can thus be used in the JWT in auth.
## Examples
@@ -37,4 +40,153 @@ defmodule RBAC do
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
+ HTTPoison.get("#{auth_url}/approles/#{client_id}")
+ |> parse_body_response()
+ end
+
+
+ # `parse_body_response/1` parses the HTTP response
+ # so your app can use the resulting JSON (list of roles).
+ defp parse_body_response({:error, err}), do: {:error, err}
+
+ defp parse_body_response({:ok, response}) do
+ body = Map.get(response, :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)
+
+ # Transform Map with strings as keys to atoms
+ # see: https://stackoverflow.com/questions/31990134
+ 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
+ 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 in-memory (ETS) for fast access.
+ """
+ def init_roles_cache(auth_url, client_id) do
+ {:ok, roles} = RBAC.get_approles(auth_url, client_id)
+ # IO.inspect(roles)
+ insert_roles_into_ets_cache(roles)
+ end
+
+ @doc """
+ `insert_roles_into_ets_cache/1` inserts the list of roles into
+ an ETS in-memroy cache for fast access at run-time.
+ ETS is a high performance cache included *Free* in Elixir/Erlang.
+ See: https://elixir-lang.org/getting-started/mix-otp/ets.html
+ and: https://elixirschool.com/en/lessons/specifics/ets
+ """
+ def insert_roles_into_ets_cache(roles) do
+ :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:
+ [] -> # :error
+ Logger.error("rbac.ex:112 Role not found in ets: #{term} \n#{Exception.format_stacktrace()}")
+ %{id: 0}
+ # role found extract role:
+ [{_term, role}] -> role
+ end
+ end
+
+ # extract the roles from String and make List of integers
+ # e.g: "1,2,3" > [1,2,3]
+ defp transform_roles_string_to_list_of_ints(roles) do
+ roles
+ |> String.split(",", trim: true)
+ |> Enum.map(&String.to_integer/1)
+ end
+
+ # allow role to be an atom for conveinece:
+ def has_role?(roles, role) when is_list(roles) and is_atom(role) do
+ role = get_role_from_cache(Atom.to_string(role))
+ Enum.member?(roles, role.id)
+ end
+
+ @doc """
+ `has_role?/2` confirms if the person has the given role.
+ e.g:
+ has_role?([1,2,42], "home_admin")
+ true
+
+ has_role?([1,2,14], "potus")
+ false
+ """
+ def has_role?(roles, role_name) when is_list(roles) do
+ role = get_role_from_cache(role_name)
+ Enum.member?(roles, role.id)
+ end
+
+ @doc """
+ `has_role?/2` confirms if the person has the given role
+ accept Plug.Conn as first argument to simply application code.
+ e.g:
+ has_role?(conn, "home_admin")
+ true
+
+ has_role?(conn, "potus")
+ false
+ """
+ def has_role?(conn, role_name) when is_map(conn) do
+ roles = transform_roles_string_to_list_of_ints(conn.assigns.person.roles)
+ has_role?(roles, role_name)
+ end
+
+ @spec has_role_any?(maybe_improper_list | %{assigns: atom | %{person: atom | map}}, any) ::
+ boolean
+ @doc """
+ `has_role_any/2 checks if the person has any one (or more)
+ of the roles listed. Allows multiple roles to access content.
+ e.g:
+ has_role_any?(conn, ["home_admin", "building_owner")
+ true
+
+ has_role_any?(conn, ["potus", "el_presidente")
+ false
+ """
+ def has_role_any?(roles, roles_list) when is_list(roles) do
+ list_ids = Enum.map(roles_list, fn role ->
+ role = if is_atom(role), do: Atom.to_string(role), else: role
+ r = get_role_from_cache(role)
+ r.id
+ end)
+
+ # find the first occurence of a role by id:
+ found = Enum.find(roles, fn rid ->
+ Enum.member?(list_ids, rid)
+ end)
+ not is_nil(found)
+ end
+
+ def has_role_any?(conn, roles_list) when is_map(conn) do
+ roles = transform_roles_string_to_list_of_ints(conn.assigns.person.roles)
+ has_role_any?(roles, roles_list)
+ end
end
diff --git a/mix.exs b/mix.exs
index 62114f3..1bc8190 100644
--- a/mix.exs
+++ b/mix.exs
@@ -4,7 +4,7 @@ defmodule Rbac.MixProject do
def project do
[
app: :rbac,
- version: "0.3.0",
+ version: "0.5.0",
elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps(),
@@ -30,9 +30,18 @@ defmodule Rbac.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
+ # Httpoison for HTTP Requests: hex.pm/packages/httpoison
+ {:httpoison, "~> 1.7.0"},
+
+ # Decoding JSON data: https://hex.pm/packages/jason
+ {:jason, "~> 1.2.2"},
+
# Check test coverage
{:excoveralls, "~> 0.13.1", only: :test},
+ # auth_plug for client_id/1: hex.pm/packages/auth_plug
+ {:auth_plug, "~> 1.2"},
+
# Create Documentation for publishing Hex.docs:
{:ex_doc, "~> 0.22.2", only: :dev},
{:credo, "~> 1.4", only: [:dev], runtime: false}
diff --git a/mix.lock b/mix.lock
index 54dc72d..1f40440 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,4 +1,5 @@
%{
+ "auth_plug": {:hex, :auth_plug, "1.2.1", "7b8af3bc119452b0da01e1f9c848d17ce62e96893c5c3c1b4eb36f3fc986c990", [:mix], [{:joken, "~> 2.2.0", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "6004562a15294f36df3fd844d1c63b776807d5c1509faa9be02d5f8e8bb123d9"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
"credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"},
@@ -6,14 +7,21 @@
"ex_doc": {:hex, :ex_doc, "0.22.2", "03a2a58bdd2ba0d83d004507c4ee113b9c521956938298eba16e55cc4aba4a6c", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "cf60e1b3e2efe317095b6bb79651f83a2c1b3edcb4d319c421d7fcda8b3aff26"},
"excoveralls": {:hex, :excoveralls, "0.13.1", "b9f1697f7c9e0cfe15d1a1d737fb169c398803ffcbc57e672aa007e9fd42864c", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b4bb550e045def1b4d531a37fb766cbbe1307f7628bf8f0414168b3f52021cce"},
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
+ "httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"},
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
- "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
+ "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
+ "joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"},
+ "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"},
"makeup": {:hex, :makeup, "1.0.3", "e339e2f766d12e7260e6672dd4047405963c5ec99661abdc432e6ec67d29ef95", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "2e9b4996d11832947731f7608fed7ad2f9443011b3b479ae288011265cdd3dad"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
+ "mime": {:hex, :mime, "1.4.0", "5066f14944b470286146047d2f73518cf5cca82f8e4815cf35d196b58cf07c47", [:mix], [], "hexpm", "75fa42c4228ea9a23f70f123c74ba7cece6a03b1fd474fe13f6a7a85c6ea4ff6"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
+ "plug": {:hex, :plug, "1.10.4", "41eba7d1a2d671faaf531fa867645bd5a3dce0957d8e2a3f398ccff7d2ef017f", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad1e233fe73d2eec56616568d260777b67f53148a999dc2d048f4eb9778fe4a0"},
+ "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
+ "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
}
diff --git a/test/rbac_test.exs b/test/rbac_test.exs
index 84d3b79..fc00851 100644
--- a/test/rbac_test.exs
+++ b/test/rbac_test.exs
@@ -59,15 +59,6 @@ defmodule RBACTest do
name: "banned",
person_id: 1,
updated_at: ~N[2020-08-19 10:04:38]
- },
- %{
- desc: "With great power comes great responsibility",
- id: 8,
- inserted_at: ~N[2020-08-19 10:04:38],
- name: "superadmin",
- person_id: 1,
- updated_at: ~N[2020-08-19 10:04:38],
- revoked: ~N[2020-08-19 10:04:38]
}
]
@@ -81,17 +72,168 @@ defmodule RBACTest do
assert RBAC.transform_role_list_to_string(roles) == roles
end
- test "this" do
- roles = %{
- __meta__: "#Ecto.Schema.Metadata<:loaded",
- desc: "Subscribes for updates e.g. newsletter",
- id: 6,
- inserted_at: ~N[2020-08-21 16:40:22],
- name: "subscriber",
- person_id: 1,
- updated_at: ~N[2020-08-21 16:40:22]
- }
+ test "transform_role_list_to_string/1" do
+ roles = [
+ %{
+ __meta__: "#Ecto.Schema.Metadata<:loaded",
+ desc: "Subscribes for updates e.g. newsletter",
+ id: 6,
+ inserted_at: ~N[2020-08-21 16:40:22],
+ name: "subscriber",
+ person_id: 1,
+ updated_at: ~N[2020-08-21 16:40:22]
+ }
+ ]
assert RBAC.transform_role_list_to_string(roles) == "6"
end
+
+ test "get_approles/2 loads the list of roles for an app" do
+ auth_url = "https://dwylauth.herokuapp.com"
+ client_id = AuthPlug.Token.client_id()
+ {:ok, roles} = RBAC.get_approles(auth_url, client_id)
+ assert length(roles) > 7
+ end
+
+ test "init_roles/2 inserts roles list into ETS cache" do
+ auth_url = "https://dwylauth.herokuapp.com"
+ client_id = AuthPlug.Token.client_id()
+ RBAC.init_roles_cache(auth_url, client_id)
+
+ # confirm full roles inserted
+ {_, list} = :ets.lookup(:roles_cache, "roles") |> List.first()
+ assert length(list) == 9
+
+ # lookup role by id:
+ role = RBAC.get_role_from_cache(1)
+ assert role.name == "superadmin"
+
+ # lookup role by name:
+ role = RBAC.get_role_from_cache("admin")
+ assert role.id == 2
+ end
+
+ # init_cache test helper function
+ def init do
+ auth_url = "https://dwylauth.herokuapp.com"
+ client_id = AuthPlug.Token.client_id()
+ RBAC.init_roles_cache(auth_url, client_id)
+ end
+
+ test "get_role_from_cache/1 cache miss (unhappy path)" do
+ init()
+ # attempt to get a non-existent role:
+ fail = RBAC.get_role_from_cache("fail")
+ assert fail.id == 0
+ end
+
+ test "RBAC.has_role?/2 returns boolean true/false" do
+ init()
+
+ fake_conn = %{
+ assigns: %{
+ person: %{
+ roles: "1"
+ }
+ }
+ }
+
+ assert RBAC.has_role?(fake_conn, "superadmin")
+ end
+
+ test "RBAC.has_role?/2 returns false when doesn't have role" do
+ init()
+
+ fake_conn = %{
+ assigns: %{
+ person: %{
+ roles: "1,2,3"
+ }
+ }
+ }
+
+ assert not RBAC.has_role?(fake_conn, "non_existent_role")
+ end
+
+
+ test "RBAC.has_role?/2 works with integers too!" do
+ init()
+
+ fake_conn = %{
+ assigns: %{
+ person: %{
+ roles: "1,2,3"
+ }
+ }
+ }
+
+ assert RBAC.has_role?(fake_conn, 3)
+ end
+
+ test "RBAC.has_role?/2 accepts List of ints as first argument" do
+ init()
+ assert RBAC.has_role?([1,2,3], 3)
+ end
+
+ test "RBAC.has_role?/2 accepts atom as second argument" do
+ init()
+ assert RBAC.has_role?([1,2,3], :admin)
+ end
+
+ test "RBAC.has_role_any?/2 conn checks if person has any of the roles" do
+ init()
+
+ fake_conn = %{
+ assigns: %{
+ person: %{
+ roles: "1,2,3"
+ }
+ }
+ }
+
+ assert RBAC.has_role_any?(fake_conn, [4, 5, 3])
+ end
+
+ test "RBAC.has_role_any?/2 List checks if person has any of the roles" do
+ init()
+ assert RBAC.has_role_any?([1,2,3], ["admin"])
+ end
+
+ test "RBAC.has_role_any?/2 List checks if person has any of the roles (List of ints)" do
+ init()
+ assert RBAC.has_role_any?([1,2,3], [3,4,5])
+ end
+
+ test "RBAC.has_role_any?/2 returns false if person doesn't have any of the roles" do
+ init()
+
+ fake_conn = %{
+ assigns: %{
+ person: %{
+ roles: "3,4,5"
+ }
+ }
+ }
+ # should not have role
+ assert not RBAC.has_role_any?(fake_conn, [2, 8, 6])
+ end
+
+ test "RBAC.has_role_any?/2 works with list of strings" do
+ init()
+
+ fake_conn = %{
+ assigns: %{
+ person: %{
+ roles: "3,4,5"
+ }
+ }
+ }
+ # should not have role
+ assert RBAC.has_role_any?(fake_conn, ["admin", "commenter", "blah"])
+ end
+
+ test "RBAC.has_role_any?/2 works with list of atoms" do
+ init()
+ assert RBAC.has_role_any?([1,2], [:admin, :commenter])
+ end
end