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

_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
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
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
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
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
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
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

Other popular topics 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
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 42920 311
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
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
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New

We're in Beta

About us Mission Statement