david234

david234

Phoenix user role based access control

I have the following routes

    get "/locations", LocationsController, :show
    get "/locations/add", LocationsController, :add
    post "/locations/add", LocationsController, :create

    get "/units", LocationsController, :show_units
    get "/unit/add", LocationsController, :add_units
    post "/unit/add", LocationsController, :create_units

    get "/products", ProductsController, :show
    get "/product/add", ProductsController, :add
    post "/products/add", ProductsController, :create_product

    get "/barcodes", ProductsController, :show_barcodes
    get "/barcode/add", ProductsController, :add_barcode
    post "/barcode/add", ProductsController, :create_barcode

I have three permission groups(admin can add more permission group)


can_admin_location
can_admin_products
can_admin_barcode

For example, You can see the below image to understand more what i am trying to achieve

I want to restrict route access based on permission given

For example if the below permission

can_admin_barcode (can use only below routes)

    get "/barcodes", ProductsController, :show_barcodes
    get "/barcode/add", ProductsController, :add_barcode
    post "/barcode/add", ProductsController, :create_barcode

Then admin can change the user access level for this permission group

can_admin_barcode (can only view barcodes and only the below route will be accessible by this user)

    get "/barcodes", ProductsController, :show_barcodes

Routes blocking must happen dynamically

I am storing current logged in user permission in session. I believe i can achieve this Route restriction using plugs.
Can someone share some insight on how to achieve this or any example. Your help is greatly appreciated

Marked As Solved

mathieuprog

mathieuprog

Assuming that the user session is stored in conn.assigns under the :current_user key (if not, just adapt) :

defmodule MyAppWeb.AuthorizationPlug do
  def init(opts), do: opts

  def call(conn, opts) do
	current_user = Map.get(conn.assigns, :current_user)
	resource = Keyword.fetch!(opts, :resource)

	authorize(conn, current_user, resource)
  end

  # below you will list all your authorization rules: 
  
  def authorize(conn, %{permissions: permissions}, :barcode_routes) do
    # check that the :can_admin_barcode permission is included in the permissions
    # if not, set status to 403 and halt the conn
    conn
  end
  
  def authorize(conn, %{permissions: permissions}, :location_routes) do
    # ...
  end
end
pipeline :ensure_barcode_routes_authorized do
  plug MyAppWeb.AuthorizationPlug, resource: :barcode_routes
end

pipeline :ensure_location_routes_authorized do
  plug MyAppWeb.AuthorizationPlug, resource: :location_routes
end

# etc

scope "/barcode", MyAppWeb do
  pipe_through [:browser, :ensure_barcode_routes_authorized]
  
  get "/", ProductsController, :show_barcodes
  get "/add", ProductsController, :add_barcode
  post "/add", ProductsController, :create_barcode
end

It’s just one way to achieve authorization over routes.

Also Liked

NduatiK

NduatiK

Continuing the discussion from Phoenix user role based access control:

You can also check out changelog.com’s approach. They define an authorize plug that allows them to define a policy for each route on a controller.

# authorize.ex

defmodule ChangelogWeb.Plug.Authorize do
  import Plug.Conn
  import Phoenix.Controller

  def init(opts), do: opts

  def call(conn, policy_module) when not is_list(policy_module),
    do: call(conn, [policy_module, nil])

  def call(conn, [policy_module, resource_name]) do
    user = conn.assigns.current_user
    resource = conn.assigns[resource_name]

    if apply_policy(policy_module, action_name(conn), user, resource) do
      conn
    else
      conn
      |> put_flash(:result, "failure")
      |> redirect(to: ChangelogWeb.Plug.Conn.referer_or_root_path(conn))
      |> halt()
    end
  end

  defp apply_policy(module, action, user, nil), do: apply(module, action, [user])
  defp apply_policy(module, action, user, resource), do: apply(module, action, [user, resource])
end

For example in the admin postcontroller these two plugs are defined.

plug :assign_post when action in [:edit, :update, :delete, :publish, :unpublish]
plug Authorize, [Policies.Admin.Post, :post]

For actions where an existing post is being manipulated, that post is first preloaded.
Then, permissions are checked on a per action basis using the Policies.Admin.Post policy which looks like:

defmodule Changelog.Policies.Admin.Post do
  use Changelog.Policies.Default

  def create(actor), do: is_admin_or_editor(actor)
  def index(actor), do: is_admin_or_editor(actor)
  def show(actor, post), do: is_admin_or_post_contributor(actor, post)
  def update(actor, post), do: is_admin_or_post_contributor(actor, post)
  def delete(actor, post = %{published: false}), do: is_admin_or_post_contributor(actor, post)
  def delete(_actor, _post), do: false

  def publish(actor, post), do: is_admin_or_post_contributor(actor, post)
  def unpublish(actor, post), do: publish(actor, post)

  # ...
end

Taken all together, when a user tries to update a post,

  1. The podcast for that id is loaded
  2. The podcast and the user are loaded in the Authorize plug. Because the resource exists ie the podcast assign is there, the update method of the post policy is called with the user and the podcast. For create, the podcast assign would be nil and the create function would be called with just the user.
  3. The policy return a bool that gives access if the user is an admin or a contributor to the post
  4. If 3 returns true, call the update method. Else reject the request
kokolegorille

kokolegorille

The plug does not know about the routes… but a simple plug like this might do

def MyPlug do
  import Plug.Conn
  def init(opts \\ []), do: opts
  def call(conn, _opts) do
    if can_admin_barcode && String.start_with?(conn.request_path, "/barcodes") do
      conn
    else
      conn 
      |> halt()
    end
    ...
  end
end
mathieuprog

mathieuprog

Yeah, that’s how I do it. You can see that I in fact attach an atom to each set of routes. Like all these barcode routes and the :barcode_routes atom.

Then in the Plug I retrieve that atom (representing thus that set of routes), and I check if the user has the right permissions for those routes.

Maybe there’s an easier way for you, but I just show you how I implemented it. This way, if you want to check something more than the permissions property in the future, there are no restrictions and you can check other user attributes.

Where Next?

Popular in Questions Top

_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lists...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New

We're in Beta

About us Mission Statement