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

2 Likes

I created a similar Plug which protects routes against authorization rules. It’s on GitHub and the source is trivial:

Let me know if it doesn’t answer your case.

Thanks. I will check now.:smiley:

I checked your code but I could able to figure out how I can use it to my situation.

For example, A user with this permission “can_admin_barcode” Can be allowed to view only below routes.
This value “can_admin_barcode” will be stored in session.

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

Can you give me insight on how to achieve this using your library.

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
1 Like

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.

1 Like

Thank you very much for your time and very neat explanation.

One small dout

My current router is like this

scope “/admin”, LogisticsWeb.Admin, as: :admin do

pipe_through [:browser, :admin, LogisticsWeb.Plugs.AuthenticateAdmin]

get "/", DashboardController, :index

resources "/admins", AdminController

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

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

end

I am changing like this. Is this a proper way to achieve this?

	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

  scope "/admin", LogisticsWeb.Admin, as: :admin do

    pipe_through [:browser, :admin, LogisticsWeb.Plugs.AuthenticateAdmin]

    get "/", DashboardController, :index

    resources "/admins", AdminController

  end

  scope "/admin/location", LogisticsWeb.Admin, as: :admin do

    pipe_through [:browser, :admin, LogisticsWeb.Plugs.AuthenticateAdmin, :ensure_location_routes_authorized]

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

  end


  scope "/admin/barcodes", LogisticsWeb.Admin, as: :admin do

    pipe_through [:browser, :admin, LogisticsWeb.Plugs.AuthenticateAdmin, :ensure_barcode_routes_authorized]

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

  end

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.

1 Like