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