From 50cce5db1d1340391f348c3fb90cddff0bd43f06 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Sun, 13 Sep 2020 23:57:20 +0100 Subject: [PATCH 01/24] add dependency on HTTPoison and Jason to make HTTP requests for /approles https://github.com/dwyl/auth/issues/110 --- mix.exs | 9 +++++++++ mix.lock | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 62114f3..ad3856e 100644 --- a/mix.exs +++ b/mix.exs @@ -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 in testsing: hex.pm/packages/auth_plug + {:auth_plug, "~> 1.2", only: [:dev, :test]}, + # 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"}, } From e05a58a7c70dcad9bd0350d516d1e01ea7c2d452 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Sun, 13 Sep 2020 23:58:00 +0100 Subject: [PATCH 02/24] create get_approles/2 function to load list of roles for an app https://github.com/dwyl/auth/issues/110 --- .gitignore | 1 + README.md | 16 +++++++++++----- lib/rbac.ex | 32 ++++++++++++++++++++++++++++++++ test/rbac_test.exs | 14 +++++++++++--- 4 files changed, 55 insertions(+), 8 deletions(-) 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/README.md b/README.md index 6629ae0..5505a20 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,7 +54,7 @@ Install by adding `rbac` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:rbac, "~> 0.1.0"} + {:rbac, "~> 0.3.0"} ] end ``` @@ -61,6 +63,10 @@ API/Function reference available at [https://hexdocs.pm/rbac](https://hexdocs.pm/rbac). +### Setup + + + ### Usage diff --git a/lib/rbac.ex b/lib/rbac.ex index 3e183ce..0888e44 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -37,4 +37,36 @@ defmodule RBAC do def transform_role_list_to_string(roles) do [Map.delete(roles, :__meta__)] |> transform_role_list_to_string() end + + @doc """ + `get_approles/1` 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) + if body == nil do + {:error, :no_body} + else # make keys of map atoms for easier access in templates + {: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 + end diff --git a/test/rbac_test.exs b/test/rbac_test.exs index 84d3b79..f040223 100644 --- a/test/rbac_test.exs +++ b/test/rbac_test.exs @@ -81,8 +81,8 @@ defmodule RBACTest do assert RBAC.transform_role_list_to_string(roles) == roles end - test "this" do - roles = %{ + test "transform_role_list_to_string/1" do + roles = [%{ __meta__: "#Ecto.Schema.Metadata<:loaded", desc: "Subscribes for updates e.g. newsletter", id: 6, @@ -90,8 +90,16 @@ defmodule RBACTest do 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 + end From 7c413539cf3348d1562770bb44dc326146a70535 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Mon, 14 Sep 2020 08:12:38 +0100 Subject: [PATCH 03/24] use :ets to store roles list in cache https://github.com/dwyl/auth/issues/110 --- .travis.yml | 4 ++-- lib/rbac.ex | 16 +++++++++++++++- mix.exs | 2 +- test/rbac_test.exs | 17 +++++++++++++++++ 4 files changed, 35 insertions(+), 4 deletions(-) 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/lib/rbac.ex b/lib/rbac.ex index 0888e44..bdd4007 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -39,7 +39,7 @@ defmodule RBAC do end @doc """ - `get_approles/1` fetches the roles for the app + `get_approles/2` fetches the roles for the app """ def get_approles(auth_url, client_id) do url = "#{auth_url}/approles/#{client_id}" @@ -69,4 +69,18 @@ defmodule RBAC do end # https://stackoverflow.com/questions/31990134 end + @doc """ + `init_roles/2 + """ + def init_roles(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 end diff --git a/mix.exs b/mix.exs index ad3856e..faba7e5 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: "1.0.0", elixir: "~> 1.10", start_permanent: Mix.env() == :prod, deps: deps(), diff --git a/test/rbac_test.exs b/test/rbac_test.exs index f040223..1a8c495 100644 --- a/test/rbac_test.exs +++ b/test/rbac_test.exs @@ -102,4 +102,21 @@ defmodule RBACTest do 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(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} = :ets.lookup(:roles_cache, 1) |> List.first() + assert role.name == "superadmin" + + # lookup role by name: + {_, role} = :ets.lookup(:roles_cache, "admin") |> List.first() + assert role.id == 2 + end end From 5b719bb8b88c3e09a96402ea48cc8ea0aa4f3e98 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Mon, 14 Sep 2020 08:23:16 +0100 Subject: [PATCH 04/24] create get_role_from_cache/1 for #1 --- lib/rbac.ex | 15 ++++++++++++++- test/rbac_test.exs | 6 ++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index bdd4007..347c8b7 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -70,7 +70,12 @@ defmodule RBAC do end @doc """ - `init_roles/2 + `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(auth_url, client_id) do {:ok, roles} = RBAC.get_approles(auth_url, client_id) @@ -83,4 +88,12 @@ defmodule RBAC do :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 + {_, role} = :ets.lookup(:roles_cache, term) |> List.first() + role + end end diff --git a/test/rbac_test.exs b/test/rbac_test.exs index 1a8c495..889bbac 100644 --- a/test/rbac_test.exs +++ b/test/rbac_test.exs @@ -112,11 +112,13 @@ defmodule RBACTest do assert length(list) == 9 # lookup role by id: - {_, role} = :ets.lookup(:roles_cache, 1) |> List.first() + role = RBAC.get_role_from_cache(1) assert role.name == "superadmin" - + # lookup role by name: {_, role} = :ets.lookup(:roles_cache, "admin") |> List.first() assert role.id == 2 end + + end From 99ec7d3a1b8771d167856d98eeee5b164d7c35b5 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Mon, 14 Sep 2020 08:45:08 +0100 Subject: [PATCH 05/24] implement has_role/2 (happy path) #1 --- lib/rbac.ex | 16 ++++++++++++++++ test/rbac_test.exs | 32 ++++++++++++++++++++++---------- test/test_helper.exs | 2 +- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index 347c8b7..1bb2e6a 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -96,4 +96,20 @@ defmodule RBAC do {_, role} = :ets.lookup(:roles_cache, term) |> List.first() role end + + @doc """ + `has_role/2 confirms if the person has the given role + """ + def has_role(conn, role_name) do + # IO.inspect(conn, label: "conn") + # IO.inspect(role_name, label: "role_name") + role = get_role_from_cache(role_name) + # IO.inspect(role) + person_roles = + String.split(conn.assigns.person.roles, ",", trim: true) + |> Enum.map(&String.to_integer/1) + + # IO.inspect(person_roles, label: "person_roles") + Enum.member?(person_roles, role.id) + end end diff --git a/test/rbac_test.exs b/test/rbac_test.exs index 889bbac..4420fec 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] } ] @@ -116,9 +107,30 @@ defmodule RBACTest do assert role.name == "superadmin" # lookup role by name: - {_, role} = :ets.lookup(:roles_cache, "admin") |> List.first() + 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(auth_url, client_id) + end + test "get_role_from_cache/1 cache miss (unhappy path)" do + + end + + test "RBAC.has_role/1 returns boolean true/false" do + init() + fake_conn = %{ + assigns: %{ + person: %{ + roles: "1" + } + } + } + assert RBAC.has_role(fake_conn, "superadmin") + end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e..e89e5ff 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1 @@ -ExUnit.start() +ExUnit.start() \ No newline at end of file From abeb6e1832e2dd55c77426f3831d37167dbd49fe Mon Sep 17 00:00:00 2001 From: nelsonic Date: Mon, 14 Sep 2020 08:53:30 +0100 Subject: [PATCH 06/24] add tests for unhappy path and refactor get_role_from_cache/1 to pass tests #1 --- lib/rbac.ex | 7 +++++-- test/rbac_test.exs | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index 1bb2e6a..2d1bd0b 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -93,8 +93,11 @@ defmodule RBAC do `get_role_from_cache/1 retrieves a role from ets cache """ def get_role_from_cache(term) do - {_, role} = :ets.lookup(:roles_cache, term) |> List.first() - role + case :ets.lookup(:roles_cache, term) do + # not found + [] -> %{id: 0} + [{_term, role}] -> role + end end @doc """ diff --git a/test/rbac_test.exs b/test/rbac_test.exs index 4420fec..28dc153 100644 --- a/test/rbac_test.exs +++ b/test/rbac_test.exs @@ -119,7 +119,10 @@ defmodule RBACTest do 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/1 returns boolean true/false" do @@ -133,4 +136,16 @@ defmodule RBACTest do } assert RBAC.has_role(fake_conn, "superadmin") end + + test "RBAC.has_role/1 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 end From ffebff516e4f182c3f04e55e15748ee95e121792 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Mon, 14 Sep 2020 08:57:58 +0100 Subject: [PATCH 07/24] mix format --- lib/rbac.ex | 38 ++++++++++++++++++++++---------------- test/rbac_test.exs | 26 ++++++++++++++++---------- test/test_helper.exs | 2 +- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index 2d1bd0b..be4c46b 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -44,6 +44,7 @@ defmodule RBAC do def get_approles(auth_url, client_id) do url = "#{auth_url}/approles/#{client_id}" HTTPoison.start() + HTTPoison.get(url) |> parse_body_response() end @@ -52,21 +53,27 @@ defmodule RBAC do `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 + @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 # make keys of map atoms for easier access in templates + 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) + + 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 + + # https://stackoverflow.com/questions/31990134 end @doc """ @@ -83,7 +90,7 @@ defmodule RBAC do # insert full list: :ets.insert(:roles_cache, {"roles", roles}) # insert individual roles for fast lookup: - Enum.each(roles, fn role -> + Enum.each(roles, fn role -> :ets.insert(:roles_cache, {role.name, role}) :ets.insert(:roles_cache, {role.id, role}) end) @@ -94,8 +101,9 @@ defmodule RBAC do """ def get_role_from_cache(term) do case :ets.lookup(:roles_cache, term) do - # not found + # not found: [] -> %{id: 0} + # extract role: [{_term, role}] -> role end end @@ -104,15 +112,13 @@ defmodule RBAC do `has_role/2 confirms if the person has the given role """ def has_role(conn, role_name) do - # IO.inspect(conn, label: "conn") - # IO.inspect(role_name, label: "role_name") role = get_role_from_cache(role_name) - # IO.inspect(role) - person_roles = - String.split(conn.assigns.person.roles, ",", trim: true) - |> Enum.map(&String.to_integer/1) - # IO.inspect(person_roles, label: "person_roles") + person_roles = + conn.assigns.person.roles + |> String.split(",", trim: true) + |> Enum.map(&String.to_integer/1) + Enum.member?(person_roles, role.id) end end diff --git a/test/rbac_test.exs b/test/rbac_test.exs index 28dc153..8a73fe3 100644 --- a/test/rbac_test.exs +++ b/test/rbac_test.exs @@ -73,15 +73,17 @@ defmodule RBACTest do end 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] - }] + 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 @@ -98,7 +100,7 @@ defmodule RBACTest do client_id = AuthPlug.Token.client_id() RBAC.init_roles(auth_url, client_id) - # confirm full roles inserted + #  confirm full roles inserted {_, list} = :ets.lookup(:roles_cache, "roles") |> List.first() assert length(list) == 9 @@ -127,6 +129,7 @@ defmodule RBACTest do test "RBAC.has_role/1 returns boolean true/false" do init() + fake_conn = %{ assigns: %{ person: %{ @@ -134,11 +137,13 @@ defmodule RBACTest do } } } + assert RBAC.has_role(fake_conn, "superadmin") end test "RBAC.has_role/1 returns false when doesn't have role" do init() + fake_conn = %{ assigns: %{ person: %{ @@ -146,6 +151,7 @@ defmodule RBACTest do } } } + assert not RBAC.has_role(fake_conn, "non_existent_role") end end diff --git a/test/test_helper.exs b/test/test_helper.exs index e89e5ff..869559e 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1 @@ -ExUnit.start() \ No newline at end of file +ExUnit.start() From 0aa6124becbf315beec718e7d671cbd21a42d48c Mon Sep 17 00:00:00 2001 From: nelsonic Date: Mon, 14 Sep 2020 09:04:33 +0100 Subject: [PATCH 08/24] add test to confirm that has_role/2 works with integers too! e.g. has_role(conn, 3) > true #1 --- lib/rbac.ex | 3 +++ test/rbac_test.exs | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/rbac.ex b/lib/rbac.ex index be4c46b..d9c624c 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -110,6 +110,9 @@ defmodule RBAC do @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) diff --git a/test/rbac_test.exs b/test/rbac_test.exs index 8a73fe3..e2aa35a 100644 --- a/test/rbac_test.exs +++ b/test/rbac_test.exs @@ -154,4 +154,19 @@ defmodule RBACTest do assert not RBAC.has_role(fake_conn, "non_existent_role") end + + + test "RBAC.has_role/1 works with integers too!" do + init() + + fake_conn = %{ + assigns: %{ + person: %{ + roles: "1,2,3" + } + } + } + + assert RBAC.has_role(fake_conn, 3) + end end From 1fb205668eb9840cc2283be8b0034e66e8c4ba95 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Mon, 14 Sep 2020 09:36:31 +0100 Subject: [PATCH 09/24] bump version to 0.4.0 with latest functions #1 --- README.md | 10 +++++++--- lib/rbac.ex | 2 +- mix.exs | 2 +- test/rbac_test.exs | 4 ++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5505a20..3812a63 100644 --- a/README.md +++ b/README.md @@ -59,20 +59,24 @@ def deps do end ``` -API/Function reference available at -[https://hexdocs.pm/rbac](https://hexdocs.pm/rbac). +### Setup -### Setup ### Usage +Once you have added the + +API/Function reference available at +[https://hexdocs.pm/rbac](https://hexdocs.pm/rbac). + +

## tl;dr > RBAC Knowledge Summary diff --git a/lib/rbac.ex b/lib/rbac.ex index d9c624c..8ef30a7 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -84,7 +84,7 @@ defmodule RBAC do See: https://elixir-lang.org/getting-started/mix-otp/ets.html and: https://elixirschool.com/en/lessons/specifics/ets """ - def init_roles(auth_url, client_id) do + 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: diff --git a/mix.exs b/mix.exs index faba7e5..f70e24f 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Rbac.MixProject do def project do [ app: :rbac, - version: "1.0.0", + version: "0.4.0", elixir: "~> 1.10", start_permanent: Mix.env() == :prod, deps: deps(), diff --git a/test/rbac_test.exs b/test/rbac_test.exs index e2aa35a..ac8fe84 100644 --- a/test/rbac_test.exs +++ b/test/rbac_test.exs @@ -98,7 +98,7 @@ defmodule RBACTest do 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(auth_url, client_id) + RBAC.init_roles_cache(auth_url, client_id) #  confirm full roles inserted {_, list} = :ets.lookup(:roles_cache, "roles") |> List.first() @@ -117,7 +117,7 @@ defmodule RBACTest do def init do auth_url = "https://dwylauth.herokuapp.com" client_id = AuthPlug.Token.client_id() - RBAC.init_roles(auth_url, client_id) + RBAC.init_roles_cache(auth_url, client_id) end test "get_role_from_cache/1 cache miss (unhappy path)" do From 90a16a82913e0173d3d5fce03e5f21e71940202b Mon Sep 17 00:00:00 2001 From: nelsonic Date: Mon, 14 Sep 2020 10:18:26 +0100 Subject: [PATCH 10/24] implement has_role_any?/2 to check if person has any of listed roles #1 --- lib/rbac.ex | 34 +++++++++++++++++++++++++---- test/rbac_test.exs | 54 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index 8ef30a7..2b5df02 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -110,11 +110,11 @@ defmodule RBAC do @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 + e.g: + has_role(conn, "home_admin") > true + has_role(conn, "potus") > false """ - def has_role(conn, role_name) do + def has_role?(conn, role_name) do role = get_role_from_cache(role_name) person_roles = @@ -124,4 +124,30 @@ defmodule RBAC do Enum.member?(person_roles, role.id) end + + @doc """ + `has_role_any_of/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?(conn, roles_list) do + list_ids = Enum.map(roles_list, fn role -> + r = get_role_from_cache(role) + r.id + end) + + # list of integers + person_roles = + conn.assigns.person.roles + |> String.split(",", trim: true) + |> Enum.map(&String.to_integer/1) + + # find the first occurence of a role by id: + found = Enum.find(person_roles, fn rid -> + Enum.member?(list_ids, rid) + end) + not is_nil(found) + end end diff --git a/test/rbac_test.exs b/test/rbac_test.exs index ac8fe84..4f7a1bb 100644 --- a/test/rbac_test.exs +++ b/test/rbac_test.exs @@ -127,7 +127,7 @@ defmodule RBACTest do assert fail.id == 0 end - test "RBAC.has_role/1 returns boolean true/false" do + test "RBAC.has_role?/1 returns boolean true/false" do init() fake_conn = %{ @@ -138,10 +138,10 @@ defmodule RBACTest do } } - assert RBAC.has_role(fake_conn, "superadmin") + assert RBAC.has_role?(fake_conn, "superadmin") end - test "RBAC.has_role/1 returns false when doesn't have role" do + test "RBAC.has_role?/1 returns false when doesn't have role" do init() fake_conn = %{ @@ -152,11 +152,11 @@ defmodule RBACTest do } } - assert not RBAC.has_role(fake_conn, "non_existent_role") + assert not RBAC.has_role?(fake_conn, "non_existent_role") end - test "RBAC.has_role/1 works with integers too!" do + test "RBAC.has_role?/1 works with integers too!" do init() fake_conn = %{ @@ -167,6 +167,48 @@ defmodule RBACTest do } } - assert RBAC.has_role(fake_conn, 3) + assert RBAC.has_role?(fake_conn, 3) + end + + test "RBAC.has_role_any?/1 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?/1 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?/1 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 end From 7dcab4458da60fa2371cad05abc3d38278d70db3 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Mon, 14 Sep 2020 10:21:00 +0100 Subject: [PATCH 11/24] tidy up docs to add ? to has_role?/2 and has_role_any?/2 #1 --- lib/rbac.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index 2b5df02..d46f28e 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -109,10 +109,10 @@ defmodule RBAC do end @doc """ - `has_role/2 confirms if the person has the given role + `has_role?/2 confirms if the person has the given role e.g: - has_role(conn, "home_admin") > true - has_role(conn, "potus") > false + 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) @@ -126,7 +126,7 @@ defmodule RBAC do end @doc """ - `has_role_any_of/2 checks if the person has any one (or more) + `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 From 1e9f126133524f6eecf7f5aa604f3ff6b82ad5c5 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Mon, 14 Sep 2020 14:59:07 +0100 Subject: [PATCH 12/24] add docs / usage example to README.md for #1 --- README.md | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3812a63..29aeb77 100644 --- a/README.md +++ b/README.md @@ -61,14 +61,107 @@ end ### Setup +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 + Auth.Repo, + # Start the endpoint when the application starts + {Phoenix.PubSub, name: Auth.PubSub}, + AuthWeb.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: Auth.Supervisor] + Supervisor.start_link(children, opts) +end +``` +Add the following code at the top of the function definition: + +```elixir +# initialize RBAC Cache: +RBAC.init_roles_cache( + "https://dwylauth.herokuapp.com", + AuthPlug.Token.client_id() +) +``` ### Usage -Once you have added the +Once you have added the initialization code, +you can easily check that a person has a required role +using the following code: + +```elixir +RBAC.has_role?(conn, "admin") +> true +``` + +Or if you want to 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: + +```elixir +RBAC.has_role_any?(conn, ["admin",2,3]) +> true +``` + +But we recommend picking one, and think 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. @@ -76,6 +169,28 @@ Once you have added the API/Function reference available at [https://hexdocs.pm/rbac](https://hexdocs.pm/rbac). +

From 012bc3dcec84c8b59401e5d6a2fe8961dd1be7e4 Mon Sep 17 00:00:00 2001 From: Tom Haines Date: Tue, 15 Sep 2020 12:55:32 +0100 Subject: [PATCH 13/24] Remove redundunt function call --- lib/rbac.ex | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index d46f28e..fb91ae9 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -43,7 +43,6 @@ defmodule RBAC do """ def get_approles(auth_url, client_id) do url = "#{auth_url}/approles/#{client_id}" - HTTPoison.start() HTTPoison.get(url) |> parse_body_response() @@ -77,9 +76,9 @@ defmodule RBAC do end @doc """ - `init_roles/2 fetches the list of roles for an app + `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. + 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 @@ -110,7 +109,7 @@ defmodule RBAC do @doc """ `has_role?/2 confirms if the person has the given role - e.g: + e.g: has_role?(conn, "home_admin") > true has_role?(conn, "potus") > false """ @@ -128,12 +127,12 @@ defmodule RBAC do @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: + e.g: has_role_any?(conn, ["home_admin", "building_owner") > true has_role_any?(conn, ["potus", "el_presidente") > false """ def has_role_any?(conn, roles_list) do - list_ids = Enum.map(roles_list, fn role -> + list_ids = Enum.map(roles_list, fn role -> r = get_role_from_cache(role) r.id end) @@ -145,7 +144,7 @@ defmodule RBAC do |> Enum.map(&String.to_integer/1) # find the first occurence of a role by id: - found = Enum.find(person_roles, fn rid -> + found = Enum.find(person_roles, fn rid -> Enum.member?(list_ids, rid) end) not is_nil(found) From c2a863c8059d533e52ff5ca0611d90d1f098631d Mon Sep 17 00:00:00 2001 From: Tom Haines Date: Tue, 15 Sep 2020 12:58:20 +0100 Subject: [PATCH 14/24] Correct wrong function signature --- lib/rbac.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index fb91ae9..7e4a66a 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -52,7 +52,7 @@ defmodule RBAC do `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() + @spec parse_body_response({atom, String.t()}) :: String.t() | {:error, term()} def parse_body_response({:error, err}), do: {:error, err} def parse_body_response({:ok, response}) do From efa5031d072733c6439cf6cb9328d7d28399071f Mon Sep 17 00:00:00 2001 From: Tom Haines Date: Tue, 15 Sep 2020 12:59:37 +0100 Subject: [PATCH 15/24] Remove func signature - I think this is easier --- lib/rbac.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index 7e4a66a..9570566 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -52,7 +52,6 @@ defmodule RBAC do `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() | {:error, term()} def parse_body_response({:error, err}), do: {:error, err} def parse_body_response({:ok, response}) do From 5a7147ca88b02d5b7c1eec64c91c53ee6fcfa34f Mon Sep 17 00:00:00 2001 From: Tom Haines Date: Tue, 15 Sep 2020 13:07:44 +0100 Subject: [PATCH 16/24] Set functions to private --- lib/rbac.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index 9570566..74de54d 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -52,9 +52,9 @@ defmodule RBAC do `parse_body_response/1` parses the response so your app can use the resulting JSON (list of roles). """ - def parse_body_response({:error, err}), do: {:error, err} + defp parse_body_response({:error, err}), do: {:error, err} - def parse_body_response({:ok, response}) do + defp parse_body_response({:ok, response}) do body = Map.get(response, :body) # IO.inspect(body) # make keys of map atoms for easier access in templates From 87f38347f40be471318352b92b36cbe490703fc5 Mon Sep 17 00:00:00 2001 From: Tom Haines Date: Tue, 15 Sep 2020 13:08:30 +0100 Subject: [PATCH 17/24] Return an explicit error --- lib/rbac.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index 74de54d..e11331f 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -100,7 +100,7 @@ defmodule RBAC do def get_role_from_cache(term) do case :ets.lookup(:roles_cache, term) do # not found: - [] -> %{id: 0} + [] -> :error # extract role: [{_term, role}] -> role end From b2b1540805f373b0679eff16577fa74f3c2eb611 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Wed, 16 Sep 2020 09:56:39 +0100 Subject: [PATCH 18/24] Log error when role not found to alert dev of typo or role.name issue https://github.com/dwyl/rbac/pull/7#discussion_r488479307 --- lib/rbac.ex | 53 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index e11331f..f6232f8 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -2,6 +2,7 @@ defmodule RBAC do @moduledoc """ Documentation for `Rbac`. """ + require Logger @doc """ Transform a list of maps (roles) to comma-separated string of ids. @@ -48,15 +49,13 @@ defmodule RBAC do |> parse_body_response() end - @doc """ - `parse_body_response/1` parses the response - so your app can use the resulting JSON (list of roles). - """ + + # `parse_body_response/1` parses the 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) - # IO.inspect(body) # make keys of map atoms for easier access in templates if body == nil do {:error, :no_body} @@ -77,13 +76,22 @@ defmodule RBAC do @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 + 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(roles) + end + + @doc """ + `insert_roles_into_ets/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(roles) do :ets.new(:roles_cache, [:set, :protected, :named_table]) # insert full list: :ets.insert(:roles_cache, {"roles", roles}) @@ -100,12 +108,23 @@ defmodule RBAC do def get_role_from_cache(term) do case :ets.lookup(:roles_cache, term) do # not found: - [] -> :error - # extract role: + [] -> # :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 get_roles_from_conn(conn) do + conn.assigns.person.roles + |> String.split(",", trim: true) + |> Enum.map(&String.to_integer/1) + end + + @spec has_role?(atom | %{assigns: atom | %{person: atom | %{roles: binary}}}, any) :: boolean @doc """ `has_role?/2 confirms if the person has the given role e.g: @@ -114,15 +133,12 @@ defmodule RBAC do """ 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) + person_roles = get_roles_from_conn(conn) Enum.member?(person_roles, role.id) end + @spec has_role_any?(atom | %{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. @@ -137,10 +153,7 @@ defmodule RBAC do end) # list of integers - person_roles = - conn.assigns.person.roles - |> String.split(",", trim: true) - |> Enum.map(&String.to_integer/1) + person_roles = get_roles_from_conn(conn) # find the first occurence of a role by id: found = Enum.find(person_roles, fn rid -> From 3f55f0e8d8042cd723cd5051d8e003421f5330d7 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Wed, 16 Sep 2020 10:21:50 +0100 Subject: [PATCH 19/24] add function defintion for has_role?/2 String, List so Plug.Conn is not assumed https://github.com/dwyl/rbac/pull/7#discussion_r488482540 --- lib/rbac.ex | 21 ++++++++++++++------- test/rbac_test.exs | 18 ++++++++++++++---- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index f6232f8..2f7e6c8 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -118,8 +118,8 @@ defmodule RBAC do # extract the roles from String and make List of integers # e.g: "1,2,3" > [1,2,3] - defp get_roles_from_conn(conn) do - conn.assigns.person.roles + defp get_roles_from_string(roles) do + roles |> String.split(",", trim: true) |> Enum.map(&String.to_integer/1) end @@ -131,12 +131,15 @@ defmodule RBAC do has_role?(conn, "home_admin") > true has_role?(conn, "potus") > false """ - def has_role?(conn, role_name) do + def has_role?(roles, role_name) when is_binary(roles) do role = get_role_from_cache(role_name) - person_roles = get_roles_from_conn(conn) - + person_roles = get_roles_from_string(roles) Enum.member?(person_roles, role.id) end + # accept Plug.Conn as first argument to simply application code + def has_role?(conn, role_name) when is_map(conn) do + has_role?(conn.assigns.person.roles, role_name) + end @spec has_role_any?(atom | %{assigns: atom | %{person: atom | map}}, any) :: boolean @doc """ @@ -146,14 +149,14 @@ defmodule RBAC do has_role_any?(conn, ["home_admin", "building_owner") > true has_role_any?(conn, ["potus", "el_presidente") > false """ - def has_role_any?(conn, roles_list) do + def has_role_any?(roles, roles_list) when is_binary(roles) do list_ids = Enum.map(roles_list, fn role -> r = get_role_from_cache(role) r.id end) # list of integers - person_roles = get_roles_from_conn(conn) + person_roles = get_roles_from_string(roles) # find the first occurence of a role by id: found = Enum.find(person_roles, fn rid -> @@ -161,4 +164,8 @@ defmodule RBAC do end) not is_nil(found) end + + def has_role_any?(conn, roles_list) when is_map(conn) do + has_role_any?(conn.assigns.person.roles, roles_list) + end end diff --git a/test/rbac_test.exs b/test/rbac_test.exs index 4f7a1bb..7762dcc 100644 --- a/test/rbac_test.exs +++ b/test/rbac_test.exs @@ -127,7 +127,7 @@ defmodule RBACTest do assert fail.id == 0 end - test "RBAC.has_role?/1 returns boolean true/false" do + test "RBAC.has_role?/2 returns boolean true/false" do init() fake_conn = %{ @@ -141,7 +141,7 @@ defmodule RBACTest do assert RBAC.has_role?(fake_conn, "superadmin") end - test "RBAC.has_role?/1 returns false when doesn't have role" do + test "RBAC.has_role?/2 returns false when doesn't have role" do init() fake_conn = %{ @@ -156,7 +156,7 @@ defmodule RBACTest do end - test "RBAC.has_role?/1 works with integers too!" do + test "RBAC.has_role?/2 works with integers too!" do init() fake_conn = %{ @@ -170,7 +170,12 @@ defmodule RBACTest do assert RBAC.has_role?(fake_conn, 3) end - test "RBAC.has_role_any?/1 checks if person has any of the roles" do + test "RBAC.has_role?/2 accepts String as first argument" do + init() + assert RBAC.has_role?("1,2,3", 3) + end + + test "RBAC.has_role_any?/2 conn checks if person has any of the roles" do init() fake_conn = %{ @@ -184,6 +189,11 @@ defmodule RBACTest do assert RBAC.has_role_any?(fake_conn, [4, 5, 3]) end + test "RBAC.has_role_any?/2 String checks if person has any of the roles" do + init() + assert RBAC.has_role_any?("1,2,3", [4, 5, 3]) + end + test "RBAC.has_role_any?/1 returns false if person doesn't have any of the roles" do init() From 1ea682cc39be950a0c54ed6f2eef3d59da603807 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Wed, 16 Sep 2020 10:23:20 +0100 Subject: [PATCH 20/24] bump version to 0.5.0 as new fn insert_roles_into_ets/1 added --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index f70e24f..c713014 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Rbac.MixProject do def project do [ app: :rbac, - version: "0.4.0", + version: "0.5.0", elixir: "~> 1.10", start_permanent: Mix.env() == :prod, deps: deps(), From 25ebaea46ba91345be7636f1191c54aae6eb11e7 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Wed, 16 Sep 2020 11:17:11 +0100 Subject: [PATCH 21/24] has_role?2 and has_role_any?2 now both accept List of Integers as first param --- lib/rbac.ex | 27 ++++++++++++--------------- test/rbac_test.exs | 13 +++++++++---- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index 2f7e6c8..3d07356 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -124,24 +124,23 @@ defmodule RBAC do |> Enum.map(&String.to_integer/1) end - @spec has_role?(atom | %{assigns: atom | %{person: atom | %{roles: binary}}}, any) :: boolean + + def has_role?(roles, role_name) when is_list(roles) do + role = get_role_from_cache(role_name) + Enum.member?(roles, role.id) + end + # accept Plug.Conn as first argument to simply application code @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?(roles, role_name) when is_binary(roles) do - role = get_role_from_cache(role_name) - person_roles = get_roles_from_string(roles) - Enum.member?(person_roles, role.id) - end - # accept Plug.Conn as first argument to simply application code def has_role?(conn, role_name) when is_map(conn) do - has_role?(conn.assigns.person.roles, role_name) + roles = get_roles_from_string(conn.assigns.person.roles) + has_role?(roles, role_name) end - @spec has_role_any?(atom | %{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. @@ -149,23 +148,21 @@ defmodule RBAC do 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_binary(roles) do + def has_role_any?(roles, roles_list) when is_list(roles) do list_ids = Enum.map(roles_list, fn role -> r = get_role_from_cache(role) r.id end) - # list of integers - person_roles = get_roles_from_string(roles) - # find the first occurence of a role by id: - found = Enum.find(person_roles, fn rid -> + 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 - has_role_any?(conn.assigns.person.roles, roles_list) + roles = get_roles_from_string(conn.assigns.person.roles) + has_role_any?(roles, roles_list) end end diff --git a/test/rbac_test.exs b/test/rbac_test.exs index 7762dcc..2cee595 100644 --- a/test/rbac_test.exs +++ b/test/rbac_test.exs @@ -170,9 +170,9 @@ defmodule RBACTest do assert RBAC.has_role?(fake_conn, 3) end - test "RBAC.has_role?/2 accepts String as first argument" do + test "RBAC.has_role?/2 accepts List of ints as first argument" do init() - assert RBAC.has_role?("1,2,3", 3) + assert RBAC.has_role?([1,2,3], 3) end test "RBAC.has_role_any?/2 conn checks if person has any of the roles" do @@ -189,9 +189,14 @@ defmodule RBACTest do assert RBAC.has_role_any?(fake_conn, [4, 5, 3]) end - test "RBAC.has_role_any?/2 String checks if person has any of the roles" do + 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", [4, 5, 3]) + 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?/1 returns false if person doesn't have any of the roles" do From 6988e2b1b107ef416f97f4465f990482245fc76d Mon Sep 17 00:00:00 2001 From: nelsonic Date: Wed, 16 Sep 2020 11:51:31 +0100 Subject: [PATCH 22/24] allow atom as role or roles in has_role/2 and has_role_any?2 respectively #7 --- lib/rbac.ex | 13 +++++++++++++ test/rbac_test.exs | 14 ++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index 3d07356..9464b4b 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -124,11 +124,23 @@ defmodule RBAC do |> 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?(conn, "home_admin") > true + has_role?(conn, "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 + # accept Plug.Conn as first argument to simply application code @doc """ `has_role?/2 confirms if the person has the given role @@ -150,6 +162,7 @@ defmodule RBAC do """ 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) diff --git a/test/rbac_test.exs b/test/rbac_test.exs index 2cee595..fc00851 100644 --- a/test/rbac_test.exs +++ b/test/rbac_test.exs @@ -175,6 +175,11 @@ defmodule RBACTest do 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() @@ -199,7 +204,7 @@ defmodule RBACTest do assert RBAC.has_role_any?([1,2,3], [3,4,5]) end - test "RBAC.has_role_any?/1 returns false if person doesn't have any of the roles" do + test "RBAC.has_role_any?/2 returns false if person doesn't have any of the roles" do init() fake_conn = %{ @@ -213,7 +218,7 @@ defmodule RBACTest do assert not RBAC.has_role_any?(fake_conn, [2, 8, 6]) end - test "RBAC.has_role_any?/1 works with list of strings" do + test "RBAC.has_role_any?/2 works with list of strings" do init() fake_conn = %{ @@ -226,4 +231,9 @@ defmodule RBACTest do # 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 From 3a902ea4e89f6a04b50ef58c52d78b1daf45a2af Mon Sep 17 00:00:00 2001 From: nelsonic Date: Wed, 16 Sep 2020 12:58:33 +0100 Subject: [PATCH 23/24] Clarify independence from auth & auth_plug in README.md https://github.com/dwyl/rbac/pull/7/files#r488471211 --- README.md | 137 ++++++++++++++++++++++++++++++++++++++++++++++------ lib/rbac.ex | 37 ++++++++------ mix.exs | 4 +- 3 files changed, 146 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 29aeb77..ed0941a 100644 --- a/README.md +++ b/README.md @@ -54,14 +54,30 @@ Install by adding `rbac` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:rbac, "~> 0.3.0"} + {:rbac, "~> 0.5.0"} ] end ``` -### Setup +
-Open the `application.ex` file of your project +### 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 @@ -69,31 +85,63 @@ def start(_type, _args) do # List all child processes to be supervised children = [ # Start the Ecto repository - Auth.Repo, + App.Repo, # Start the endpoint when the application starts - {Phoenix.PubSub, name: Auth.PubSub}, - AuthWeb.Endpoint + {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: Auth.Supervisor] + opts = [strategy: :one_for_one, name: App.Supervisor] Supervisor.start_link(children, opts) end ``` -Add the following code at the top of the function definition: +Add the following code at the top of the `start/2` function definition: ```elixir -# initialize RBAC Cache: +# 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 @@ -101,12 +149,73 @@ 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 ``` -Or if you want to check that the person has has any role in a list of potential roles: +Check that the person has has any role in a list of potential roles: ```elixir RBAC.has_role_any?(conn, ["admin", "commenter"]) @@ -138,14 +247,14 @@ RBAC.has_role_any?(conn, [1,2,3]) > true ``` -You can even _mix_ the type in the list: +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 ``` -But we recommend picking one, and think advise using strings for code legibility. +We recommend picking one, and advise using strings for code legibility. e.g: ```elixir @@ -162,7 +271,7 @@ 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. @@ -225,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 9464b4b..3cb93a0 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -43,9 +43,7 @@ defmodule RBAC do `get_approles/2` fetches the roles for the app """ def get_approles(auth_url, client_id) do - url = "#{auth_url}/approles/#{client_id}" - - HTTPoison.get(url) + HTTPoison.get("#{auth_url}/approles/#{client_id}") |> parse_body_response() end @@ -81,17 +79,17 @@ defmodule RBAC do 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(roles) + insert_roles_into_ets_cache(roles) end @doc """ - `insert_roles_into_ets/1 inserts the list of roles into + `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(roles) do + 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}) @@ -131,22 +129,28 @@ defmodule RBAC do end @doc """ - `has_role?/2 confirms if the person has the given role + `has_role?/2` confirms if the person has the given role. e.g: - has_role?(conn, "home_admin") > true - has_role?(conn, "potus") > false + 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 - # accept Plug.Conn as first argument to simply application code @doc """ - `has_role?/2 confirms if the person has the given role + `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 + has_role?(conn, "home_admin") + true + + has_role?(conn, "potus") + false """ def has_role?(conn, role_name) when is_map(conn) do roles = get_roles_from_string(conn.assigns.person.roles) @@ -157,8 +161,11 @@ defmodule RBAC do `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 + 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 -> diff --git a/mix.exs b/mix.exs index c713014..1bc8190 100644 --- a/mix.exs +++ b/mix.exs @@ -39,8 +39,8 @@ defmodule Rbac.MixProject do # Check test coverage {:excoveralls, "~> 0.13.1", only: :test}, - # auth_plug for client_id/1 in testsing: hex.pm/packages/auth_plug - {:auth_plug, "~> 1.2", only: [:dev, :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}, From ad51cc1365cadf4926bb0fe65a7f1a4f72841a0b Mon Sep 17 00:00:00 2001 From: nelsonic Date: Wed, 16 Sep 2020 13:18:16 +0100 Subject: [PATCH 24/24] tidy @doc comments for consistency --- lib/rbac.ex | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/rbac.ex b/lib/rbac.ex index 3cb93a0..0e4ca0a 100644 --- a/lib/rbac.ex +++ b/lib/rbac.ex @@ -5,7 +5,9 @@ defmodule RBAC do 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 @@ -48,7 +50,7 @@ defmodule RBAC do end - # `parse_body_response/1` parses the response + # `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} @@ -60,6 +62,8 @@ defmodule RBAC do 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} @@ -67,12 +71,10 @@ defmodule RBAC do {:ok, atom_key_map} end - - # https://stackoverflow.com/questions/31990134 end @doc """ - `init_roles/2 fetches the list of roles for an app + `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. """ @@ -83,7 +85,7 @@ defmodule RBAC do end @doc """ - `insert_roles_into_ets_cache/1 inserts the list of roles into + `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 @@ -101,7 +103,7 @@ defmodule RBAC do end @doc """ - `get_role_from_cache/1 retrieves a role from ets cache + `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 @@ -116,7 +118,7 @@ defmodule RBAC do # extract the roles from String and make List of integers # e.g: "1,2,3" > [1,2,3] - defp get_roles_from_string(roles) do + defp transform_roles_string_to_list_of_ints(roles) do roles |> String.split(",", trim: true) |> Enum.map(&String.to_integer/1) @@ -153,10 +155,12 @@ defmodule RBAC do false """ def has_role?(conn, role_name) when is_map(conn) do - roles = get_roles_from_string(conn.assigns.person.roles) + 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. @@ -182,7 +186,7 @@ defmodule RBAC do end def has_role_any?(conn, roles_list) when is_map(conn) do - roles = get_roles_from_string(conn.assigns.person.roles) + roles = transform_roles_string_to_list_of_ints(conn.assigns.person.roles) has_role_any?(roles, roles_list) end end