Authorizing context actions where? In each function, or the controller?

Wow. Well, I can’t say that it’s the wrong call given the importance of security to their business, but for most shops, not using an auth framework, and instead writing completely new apps for each ‘role’ doesn’t strike me as a great balance of cost/benefit.

Thank you for adding this link to the conversation, @cleaver. Definitely not something I’d considered, and it’s always good to hear new thinking/alternative approaches.

I haven’t really thought it over that much, but I wonder how much more difficult it would be to build two apps. I’m not about to change my approach mid-project, but it seems like it could save a lot of overhead.

Considering the extra complexity, QA, and security testing required for the single app, the difference may not be so great. Also, the impact of a security breach is a factor. Are we talking minor embarrassment, or $millions lost?

Since this is a frequent and important question that has come up a lot recently, I’ve written a blog post around it outlining my thoughts on where to handle authorization under different use cases:

4 Likes

I do something pretty similar for authorization. I have very fine grained permissions (requested of rme) and I have a singular Permission module that just has a few helpers on it like is_logged_in/1 and can/2 and so forth.

Just about every single one of my ‘contexts’ (though they predate that phoenix concept by a great deal) take an env as their first argument and return an env as their return type (either alone if nothing else is needed or as a tuple with other values, or an %Exception{} structure is returned, yes returned not thrown). An example is, let me just grab a random one since I have a file open:

  def get_requirements(env) do
    query =
      from r in DB.SomeModule.Requirement,
      where: is_nil(r.removed_at)

    reqs =
      query
      ~> Repo.all()
      ~> Enum.filter(&can?(env, %Permissions.SomeModule.Requirement{action: :index, id: &1.id}))

    {env, reqs}
  end

So a permission itself is just a struct that fulfills a behaviour for some helper functions on it (introspection of them, like valid values and so forth for given keys).
I can test a permission with either can(env, somePermission) -> env | %Exception{} or can(env, somePermission) -> true | false, in the above case I’m using the can? variant (actually not commonly used overall, the can is the most often used) to test each of these records to test if the current user has access for each record. The above function is used as such from a controller like:

  def index(conn, _params) do
    conn
    ~> can(%Permissions.SomeModule.Requirement{action: :index})
    ~> SomeModule.get_requirements()
    ~> case do {conn, reqs} ->
      render(conn, :index, requirements: reqs)
    end
  end

As you can see a conn is an ‘environment’ (as are sockets and a few other things depending on access, I only have to implement a couple things to support something else as an ‘environment’) and as such it gets threaded through all the calls.

Another pattern I often use would be like using it as such:

  def index(conn, _params) do
    conn
    ~> can(%Permissions.SomeModule.Requirement{action: :index})
    ~> SomeModule.get_requirements()
    ~> pipe2(do_something(42))
    ~> ...
  end

Where pipe2 just takes the tuple that is passed as the value and folds it into the argument positions, so the above is the same thing as doing (it literally compiles into this):

  def index(conn, _params) do
    conn
    ~> can(%Permissions.SomeModule.Requirement{action: :index})
    ~> SomeModule.get_requirements()
    ~> case do {v0, v1} -> do_something(v0, v1, 42) end
    ~> ...
  end

And since I try to follow the same ‘pattern’ of things around then it fits in quite well so everything just becomes easily pipeable. Most of the code everywhere exists within long streams of ~>'s (a few |>'s still around too). It is very pleasant to keep up and maintain. :slight_smile:

Only thing I wish for is a static type checker to better decorate things. ^.^

I keep trying to use with, but I really really hate this format that most seem to use:

with blah <- do_something1(),
     bloop when is_atom(bloop) <- do_something2(),
     bleep <- do_something3(bloop, blah),
     do: {:ok, bleep},
     else: err -> handle_error(err)

Or:

with blah <- do_something1(),
     bloop when is_atom(bloop) <- do_something2(),
     bleep <- do_something3(bloop, blah) do
  {:ok, bleep}
else
  err -> handle_error(err)
end

Or:

with\
  blah <- do_something1(),
  bloop when is_atom(bloop) <- do_something2(),
  bleep <- do_something3(bloop, blah),
  do: {:ok, bleep},
  else: err -> handle_error(err)

Or other variants, they all just itch me very very wrong. The last one is the one I use when I am forced to use with due to too lazy to dep-in something better as the everything having comma’s at least makes copy/pasting and moving lines around easier, but holy heck it looks horrible. I’d still prefer:

with do
  blah <- do_something1()
  bloop when is_atom(bloop) <- do_something2()
  bleep <- do_something3(bloop, blah)
  {:ok, bleep} # Oh looky, last expression is the returned one, like everywhere else in elixir-land...
else
  err -> handle_error(err)
end

That style just makes so much more sense to me than arbitrary and undefined amount of argument passing in that you have to split among multiple lines to get it even remotely readable… And yes, packages exist that do precisely this, which just makes the built-in with soooo weird, this weird oddity or wart on an otherwise more sensical syntax (excepting for, but I have my own notes on how it should have been done too, they are both weird).

3 Likes

@OvermindDL1 can you explain in some more detail what your SomeModule.get_requirements/1 does? What are “requirements” in this context? (the overall approach sounds quite interesting)

Heh, the name makes more sense if I did not replace the real module name with SomeModule, it is purely related to that module and is not a ‘system’ thing. It is ‘requirements’ that a user needs to fulfill, if they do not then emails get sent. This function just gets the list that the logged in person is allowed to see.

The actual code of get_requirements/1 is just this:

  • I build a query (I have a habit of doing bindingname =\n\s\s for almost everything, as you see here)
  • Then a newline to separate ‘actions’ (I keep distinct pipelines separated by empty lines for visibility).
  • Then I build the return reqs by taking the query (I could pass it straight along, and in this function it would make sense too, but in other functions of this style the query gets altered by a variety of other calls based on passed in arguments, I’m not doing that here just so it follows the same pattern of the other functions), feeding it to Repo.all() (in this context ~> is identical to |>, specifically ~> is identical to |> except when the value is some kind of error of the form of an %Exception{} or :error or {:error, reason}, in which case then it skips the call and passes the error straight on), then I just filter out the returned records by testing if the current user has access to ‘index’ it (In other words they can look up information on it) with the given id of the requirement record…
  • Then a newline and I return the env and result in a 2-tuple.
1 Like

I’ve done both authorization at the controller level, and authorization at the context level and prefer to do it at the controller level too!

It keeps context functions plain and simple and take the guess work out of testing and building out larger features with these functions.

onboard_user(%MyApp.User{}) will definitely onboard the user, it doesn’t check if the user calling the function is allowed to onboard new users. He’s already been given an ocular patdown by the Phoenix controller.