Hi, I build my own authorization code. So, for my needs, I have 3 kinds of authorization:
- Role authorization (specific roles for specific actions, admin or customer service can look users or orders data, etc).
- Ownership authorization (user can only reads and updates his/her own orders, profile, password, posts, etc).
- Combination of above.
I previously used canary, but now I just write my own authorization code.
I have this very first iteration of code. There are 3 public functions. Note, that I previously set current user and its role beforehand (via a plug). (I have a user table and a role table, user table has role_id).
verify_role
, to verify if user has a role included as permitted roles.
verify_owner
, to verify ownership of user’s data.
verify_owner_or_role
, combination of above, just for convenience.
They all return {:ok}
, if verified, otherwise they will return {:error, status}
, which will be received by FallbackController
.
For my case, I prefer checking the authorization on the controller. Something like this:
def show(conn, %{"id" => id}) do
with {:ok} <- verify_owner_or_role(conn, id, ["admin", "customer_service"]) do
user = Accounts.get_user!(id)
render(conn, "show.json", user: user)
end
end
It means that to show a specific user data with id 1, conn must have current_user to be the user with id 1, OR current_user is an admin or a customer service. Your case may vary.
defmodule WshopWeb.ApiSecurity.UserVerification do
def verify_role(conn, permitted_roles) do
with {:ok, conn} <- authenticate(conn),
{:ok, conn} <- authorize(conn, permitted_roles) do
{:ok}
else
{:error, status} ->
{:error, status}
end
end
def verify_owner_or_role(conn, user_id, permitted_roles) do
case verify_role(conn, permitted_roles) == {:ok} || verify_owner(conn, user_id) == {:ok} do
true -> {:ok}
false -> {:error, :forbidden}
end
end
def verify_owner(conn, user_id) do
with {:ok, conn} <- authenticate(conn),
{:ok, conn} <- do_verify_ownership(conn, user_id) do
{:ok}
else
{:error, status} ->
{:error, status}
end
end
defp do_verify_ownership(conn, user_id) do
case conn.assigns.current_user.id == user_id do
true -> {:ok, conn}
false -> {:error, :forbidden}
end
end
defp authenticate(conn) do
if conn.assigns.current_user do
{:ok, conn}
else
{:error, :unauthorized}
end
end
defp authorize(conn, permitted_roles) when permitted_roles == [], do: {:ok, conn}
defp authorize(conn, permitted_roles) do
case role_permitted?(conn, permitted_roles) do
true -> {:ok, conn}
false -> {:error, :forbidden}
end
end
defp role_permitted?(conn, permitted_roles) do
assigns = conn.assigns
Map.has_key?(assigns, :role) && assigns.role in permitted_roles
end
end
There are also 2 other cases:
- For a case which a model can be updated by different user roles, like in Uber/Lyft case, when an order is placed by a customer who can also change the state of the order (create, cancel, etc) but the states (from pickup to finished) are updated by a driver. I could write another function, but I haven’t. Something like
verify_picked_order
. I should have renamed above module to be like UserJourneyVerification
while that verify_picked_order
could be placed in another module. Note that code above is my first iteration, I should split the authorization for admin/customer service or whatever other than the customer, into another module.
- For a case of which an authorization is done on grouping. Let say a teacher can look into his students data. I should create
verify_role_and_owner
, which means a teacher must have a role of teacher (or whatever the business requirements demand) and we query the database to check if a teacher teach specific students, so we can show the students data.
I don’t know if this approach is good or not. I like this approach, it is just a group(s) of functions.
Critics and suggestions are welcome.