Hello, I need some help architecting this the right way.
Let’s say that I have different roles (teacher, student, secretary) and different routes
scope "/", MyAppWeb do
get "/votes", Votes
get "/lessons", Lessons
get "/admin", Admin
get "/enroll", Enroll
end
And I also have a plug EnsureRole
where I can pass the list of roles allowed to be logged in.
I want the following:
- the resource
/votes
should be accessible by teachers, secretaries and students
- the resource
/lessons
should only be accessible by teachers and students
- the resource
/admin
should only be accessible by teachers and secretaries
- the resource
/enroll
should only be accessible by students and secretaries
I have tried to create different pipelines like this:
pipeline :teacher do
plug MyAppWeb.EnsureRolePlug, ["teacher"]
end
pipeline :teacher_and_student do
plug MyAppWeb.EnsureRolePlug, ["teacher", "student"]
end
# and so on...
and then have the following
scope "/", MyAppWeb do
scope "/", :teacher_and_student do
pipe_through :teacher_and_student
get "/lessons", Lessons
end
# and so on...
end
but I was wondering if there is a cleaner and less repetitive way to do this (especially for the resource where multiple roles can have access)?
Take a look at the README of https://github.com/schrockwell/bodyguard. Even if you don’t want to use that package, it’s pretty informative on the subject.
1 Like
Have you tried something like this?
scope "/", MyAppWeb do
pipe_through :teacher_and_student
get "/lessons", Lessons
end
scope "/", MyAppWeb do
pipe_through :teacher_and_secretaries
get "/admin", Lessons
end
This is the way I was going with.
The problem is that in the future, as the business requirements change (this was a dummy example) I could have all combinations of the roles, and I was looking for a cleaner and more composible way
1 Like
Another solution is to let auth requirements at controller level:
defmodule MyWeb.PostController do
plug :auth when action in ~w|show create update|a
def show(conn, params) do
# ...
end
end
Instead of using a new plug for each combination, how about creating a single auth plug that maps routes to lists of role names that are allowed to access it? I did something similar to migrate a rails pundit based auth system to phoenix and its working well so far.
1 Like
What you need is a simplified ABAC system (attribute based authorization system), you can do it with a simple plug like this:
defmodule AppWeb.AbacPlug do
import Plug.Conn
# set of path permissions for each role
@admin_perms ~w(lessons admin)
@teacher_perms ~w(lessons grading)
@student_perms ~w(lessons)
def init(options), do: options
def call(conn, _opts) do
# getting the first part of the path after the domain
path_head = hd(conn.path_info)
user_perms = get_user_perms(conn)
case Enum.member?(user_perms, path_head) do
true ->
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, "YAY!\n")
false ->
conn
|> put_resp_content_type("text/plain")
|> send_resp(403, "NAY")
end
end
# Get the user role and perms from the conn here
# in this example the perms are hardcoded.
defp get_user_perms(_conn) do
user_role = "student"
case user_role do
"admin" -> @admin_perms
"teacher" -> @teacher_perms
"student" -> @student_perms
end
end
end
You can get pretty far with an ABAC, setting read, update, create and delete permissions separately, hour of the day when each action is permitted and so on, there’s a few libraries that can help you with that, but for the most basic case this plug will work.
This approach also allows you to model your user with dynamic permissions, so you can have an admin that has access to a part of the admin that other admins don’t without having to create a new role only for that.
3 Likes