Permission and Access Manager

What is the best way to build permission and acces manager?

I built custom Plug which get current_user from Pow, route_info from Phoenix and get_path from conn. But there is few issue.

My code looks like this one

defmodule MyApp.Plug.CheckPermission do

  # ... some logic

  defp proceed?(conn) do
    user = PowPlug.current_user(conn)
    [context, module] = get_path(conn)
    action = get_action(conn)
    permission = "#{context}.#{module}.#{action.plug_opts}"
    AccessManager.has_perm?(user.permissions, permission)
  end

  def get_action(conn) do
    Phoenix.Router.route_info(
      GamblerWeb.Router,
      conn.method,
      conn.request_path,
      GamblerWeb.Endpoint.host()
    )
  end

  defp get_path(conn) do
    case conn.path_info do
      [] -> ["", ""]
      [context] -> [context, ""]
      [context, module | _rest] -> [context, module]
    end
  end

It works well if all of my routes look like this /<context>/<schema>/<other_content>.

Our app is getting bigger and bigger so i decided to refactor my router.ex file. And it was huge. Short story,

App is built as 4-parts project.

  1. Internal API to communicate with our front app based on angular/vue/react
  2. External API to communicate with external system like payments provider or external source
  3. Admin Console as simple CRUD for users
  4. Ajax API to build advanced pages like dynamic category tree

I read that it is common to use few scopes

  scope "/api/internal/v1", AppWeb, as: :api_internal do
    pipe_through :api_internal
    # ...
  end

  scope "/api/external/v1", AppWeb, as: :api_external do
    pipe_through :api_external
    # ...
  end


  scope "/api/ajax/v1", AppWeb, as: :api_ajax do
    pipe_through :api_ajax
    # ...
  end


  scope "/", AppWeb do
    pipe_through :html
    # ...
  end

And know i dont know how to pass our permission to ajax scope. It’s important to check user permission on ajax requesty, but our solution doesn’t allow to start path from /api/ajax.


Is there any good option?


For example we could have 3 context. All of them, except permissions are basic CRUD operation

Accounts.Account [:create, :read: update, :delete]
Account.Permissions [:toggle]
Blog.Category [:create, :read: update, :delete]
Blog.Post [:create, :read: update, :delete]

Examples user permission:

# this user can do anything on category
[
"blog.category.create",
"blog.category.update",
"blog.category.read",
"blog.category.delete",
]

How to deal with it?

And second question. It is good option to separate directory like below?

controllers/api_external/accounts/account.ex
controllers/api_external/accounts/permissions.ex
controllers/api_external/blog/category.ex
controllers/api_external/blog/post.ex
controllers/api_internal/accounts/account.ex
controllers/api_internal/accounts/permissions.ex
controllers/api_internal/blog/category.ex
controllers/api_internal/blog/post.ex
controllers/api_ajax/accounts/account.ex
controllers/api_ajax/accounts/permissions.ex
controllers/api_ajax/blog/category.ex
controllers/api_ajax/blog/post.ex
controllers/html/accounts/account.ex
controllers/html/accounts/permission.ex
controllers/html/blog/category.
controllers/html/blog/post.ex

The same on view.

Actual i have only api_external, api_internal and html + ajax. But there is one issue. Default traceback on 404 page in html scope render 404 page instead json 404 page. It is possible to fix by adding case or with but it is still lot of redundant logic.


So there is few topic in one post, but they are closely related.

  1. scope in route - best practice
  2. directories path - best practice
  3. custom plug and acces manager - how to get current path and action

Probably off-topic, but if you are on postgres, role level security might be an option.

I have to build a whole permission manager. I mean admin can grant permission for other users, so low level permission based on postgress could be not enough.

But thanks a lot! I will read it and check. Mayby is there any clue :slight_smile:

It’s possible for admin to grant permissions to users with RLS as well. The way I use it is I assign user id to current transaction and then use it in RLS policies.

defmodule App.Repo do
  use Ecto.Repo,
    otp_app: :app,
    adapter: Ecto.Adapters.Postgres

  defp auth_tx(%_Auth{user_id: uid, rls_role: role}) do
    query!("set local role #{role}")
    query!("set local #{key} to '#{value}'")
    :ok
  end

  # rls_* functions can be auto-generated since almost all of them follow the same flow
  def rls_get(schema, id, authable, opts \\ []) do
    {:ok, result} =
      transaction(fn ->
        :ok = auth_tx(authable)
        get(schema, id, opts)
      end)

    result
  end
end
1 Like

The way it works in the app is

def get_resource(user, id) do
  Repo.rls_get(Resource, id, user)
end

So pretty much all Repo.* functions become Repo.rls_*, and the access rules are enforced at DB level.

Example policy for the resource that belongs only to those who have been given access to it by an admin:

CREATE POLICY normal ON resources TO app_normal
  USING (id IN (
    SELECT resource_id FROM permitted_resources WHERE user_id = current_setting('app.uid')::uuid
  ));

This limits the resources only to those that are in permitted_resources join table. Postgres docs explain policies very well.

Looks nice. Thanks!

Last question ja how to get user in context.
My solution is based on plug so i can get it from conn. But your solution require to pass user from controller to context.

I’ll try and let u know. Thanks again!

Not sure I understand fully, but yes, in my approach I always pass the current user into the context functions and I also get it from plug’s conn or liveview’s socket. Passing the current user to context is the suggested approach by liveview anyway because security.

Passing the user into context functions directly rather than implicitely makes testing easier as well.

True. But its easy to forget. I know because I have already forgot about it in most of the system

You can setup warning system for non-authed operations during compilation. You can also use a different module for rls_ functions and warn on direct Repo calls. Dialyzer helps with it as well, since most functions would need to have a user type as first arg.

OK. It looks nice. But what about presentation.

Some resources should be disbaled or hidden from interface if i don’t have permission.
I cant hide them if all my permission are based on postgres.

Resolving permissions by url looks bad. Is there any better options? Mayby by passing context/schema to controller and read them in PLUG? Is this even possible?

Why not? If the user doesn’t have a permission to access a row, it won’t be returned to them in selects.

Resolving permissions by url looks bad. Is there any better options? Mayby by passing context/schema to controller and read them in PLUG? Is this even possible?

What do you mean by resolving permissions by url? I’m not using urls in my examples above.

Not you. I said it at the beggining of this topic.

Permission based on RLS seems great. But still i don’t know how to hide something in menu. For example you don’t have permission for user. How should i remove users card from menu?

And second question is how to deal with it on web context? Like i said i build it by PLUG.
In PLUG i get user from POW, path from url and action from request. So in my solution there is no information about context, schema or even table. And my solution is based on paths.

I’m trying to understand how to implement your solution. System will raise exception if user doesnt have permission to current resource?

A particular user or a users table? If you have multiple roles, you get the current user role and draw the UI based on it. Otherwise a select from the table would do, as it’d only return the resources the current user has access to.

And second question is how to deal with it on web context?

Same way, you get a user in plug from a cookie, it can be a user id or a session token.