jeffdeville

jeffdeville

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

@schrockwell - I wasn’t sure if it’d be better to ask this here, or as a github issues, but thought more input would be sourced this way.

v2 of Bodyguard was recently released GitHub - schrockwell/bodyguard: Simple authorization conventions for Phoenix apps · GitHub

It’s AWESOME for authorization in phoenix 1.3. Highly recommended.

I do have one question that’s more about the docs and the 1.3 Phoenix release than anything else though.

Where do you CALL your authorization logic? The examples for bodyguard suggest doing so in the controller. That’s how things were done before, and it certainly keeps methods that otherwise don’t have any need for the user object to not require it. But it also makes it really easy to skip your authorization calls (inside or outside of your phoenix app). So it seems like forcing authorization would be ideal.

One other option would be to look at authorization from an AoP point of view. To do that effectively in phoenix, I think you’d need to find a way to store the current user on each call, and then wrap all of your auth-requiring methods in macros that will check your policies before executing the underlying code.

I saw a great article on Function Decorators here that could be used.

The negative to this approach is just that part about having to set ‘invisible’ data as context to a function. It never bothered me in OOP land, but it feels anti-functional here.

Most Liked

schrockwell

schrockwell

Hi @jeffdeville – you’ve raised a good question. I went back-and-forth internally for a while about this. I think we can at least agree that, whether called internally or externally, the authorize/3 callback should exist directly on the context module itself, since it’s a context-specific API that determines what user can do.

Off the top of my head, here are some arguments for performing authorization in a controller action:

  • Reinforces the concept of controllers being the interface INTO your app – the first line of defense
  • Easier to call context methods from a privileged position (e.g. scheduled task, testing, etc) where we don’t care about authorization
  • Don’t need to pass the user (e.g. conn.assigns[:current_user]) into every context function if not needed (although lots of the time, we end up doing that anyway)
  • Easier to compose multiple context actions together
  • Leaner context functions that just “do the thing”

… and some arguments for performing authorization in a context function:

  • Strictly enforces authorization rules at application level – can’t skip it, no matter what. Arguably this is better design, and forces you to think about non-user-account cases like “guest” or “background task” users
  • Leaner controller actions
  • More flexibility to perform complex authorization rules, or change authorization rules without having to track down every context caller

So while I did pick controller-level authorization for the code examples, I don’t think it’s the One True Way, and the overall design is certainly up to you.

I think there is room in Bodyguard for a design element that gives you the best of both worlds – a way to perform authorization from within a context function (better design) but doesn’t require repetitive auth checks and passing the user model around everywhere (more convenient). I haven’t thought it through so I’m open to suggestions.

chrismccord

chrismccord

Creator of Phoenix

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:

OvermindDL1

OvermindDL1

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).

Where Next?

Popular in Questions Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: &lt;h1&gt;Create Post&lt;/h1&gt; &lt;%= ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 47930 226
New

We're in Beta

About us Mission Statement