schrockwell / bodyguard

Simple authorization conventions for Phoenix apps

Home Page:https://hexdocs.pm/bodyguard/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Phoenix 1.3

zimt28 opened this issue Β· comments

With Phoenix 1.3 soon to be released, I was wondering how bodyguard should be used with it, as we will have more separation and structure (aka contexts) in the new version.

I'll now port my app over to the current 1.3.0-rc.0 and share my experience when I get to authorization, but I was wondering if you already thought about this :)

I actually have given it some thought, and I'm glad you brought it up. :) Check out the readme on the 2.0-dev branch: https://github.com/schrockwell/bodyguard/tree/2.0-dev and the PR: #22 for proposed changes.

The idea is to move authorization away from schemas (MyApp.MySchema.Policy) and instead move it up to the context level (MyApp.MyContext.Policy) to protect each context function. I'm pretty confident in this decision, and think the new policy structure will support it well.

Conversely, scope callbacks are typically limited to a particular schema type, and so the scoping should probably occur from within the context. I don't like the solution currently on 2.0-dev, so any suggestions on how to approach that would be appreciated.

Since scoping is very schema-centric, maybe it makes sense to just use the schema module itself?

defmodule MyApp.MyContext.MyModel do
  use Ecto.Schema
  use Bodyguard.Schema # injects function: scope(user, scope, opts)

  schema "my_model" do
    # ...
  end

  def filter(user, scope, params) do # callback for scope/3
    # return filtered scope
  end
end

MyApp.MyContext.MyModel.scope(user) # scope arg defaults to MyApp.MyContext.MyModel
MyApp.MyContext.MyModel.scope(user, queryable)
MyApp.MyContext.MyModel.scope(user, queryable, param: :value)


def MyApp.MyContext do
  alias MyApp.MyContext

  def list_my_models(user) do
    user
    |> MyContext.MyModel.scope
    |> Repo.all
  end

  def list_new_my_models(user) do
    query = from mm in MyContext.MyModel, where: mm.status == "new"

    user
    |> MyContext.MyModel.scope(filter)
    |> Repo.all
  end
end

I dunno. Bodyguard doesn't really bring a lot to the table any more with regards to scoping, since it's not really doing any time-saving magic, and the callback structure is more mental overhead.

Since this problem lives outside the domain of true authorization checks, I'm inclined just to drop scopes altogether, and leave it up to the app.

One issue I see right now (with authorization) is that contexts shouldn't know about other contexts' schemas. This makes it impossible to pattern match on user and resource using their structs unless they're in the same context. So I think we have to break or at least bend the rules a bit to make this work, don't we?

Edit: Would it be a better idea to build one auth* context then and include all policies there? This would keep the "cheating" modules in one place.

One other thing to keep in mind is that people might build auth* into their app, or their interface. This brings up the question what's the best place to do it, I'd guess it would be the application itself as auth* would then work for all interfaces, but then the user would need to be passed with every context function call. In this case canada might just be enought. bodyguard however just covers the implementation in the interface, right? This approach, compared to the one mentioned before, seems more convenient though.

# In a controller, authorize an action
with :ok <- MyApp.Blog.Policy.authorize(conn, :update_post, post: post) do
  # ...
end

What does bodyguard do here other than getting the current user from conn and then calling the permit functions? πŸ€”

Please excuse if my questions and suggestions might not make much sense, it's all theory and I haven't played with 1.3 much yet :)

One issue I see right now (with authorization) is that contexts shouldn't know about other contexts' schemas. This makes it impossible to pattern match on user and resource using their structs unless they're in the same context.

I had a good discussion about this on the Elixir slack last night. The consensus was that yes, the schemas should not refer to each other, and to avoid this each context should have their OWN version of the schema with only the fields that context cares about, e.g. MyApp.MySchema.User and MyApp.MyOtherSchema.User. Underneath they can refer to the same table in your DB.

So in that case, doing context-centric authorization still works OK, without violating context boundaries.

What does bodyguard do here other than getting the current user from conn and then calling the permit functions?

Basically nothing.

Although now I see even that piece of magic is disingenuous, because of the context boundary problem. Which User type should live on the conn?

This brings up the question what's the best place to do it, I'd guess it would be the application itself as auth* would then work for all interfaces, but then the user would need to be passed with every context function call

Maybe instead of a separate Policy module, the permission checks just live in the context itself. It's already a giant bag of functions, so might as well throw authorization on there.

defmodule MyApp.MyContext do
  use Bodyguard.Policy

  def permit(user, action, params), do: # ...
end

with :ok <- MyApp.MyContext.authorize(conn, :some_action),
  {:ok, result} <- MyApp.MyContext.some_action(), do: # ...

Bodyguard is still a pretty thin (unnecessary?) layer at that point though.

I wonder if a @behaviour is most appropriate at that point. Specify authorize/3 as a behaviour and inject authorize!/3 and authorize?/3 wrappers for convenience.

Instead of magically unwrapping the assigns[:current_user] from a the conn, maybe provide a little macro to override the action/2:

def MyApp.Web.MyController do
  use Phoenix.Web, :controller
  require Bodyguard.Controller

  # Override action/2 to pull out conn.assigns.current_user as a 3rd arg
  extract_action_user() 

  def index(conn, params, current_user) do # <-- new current_user arg
    with :ok <- MyApp.MyContext.authorize(current_user, :list_things),
      {:ok, result} <- MyApp.MyContext.list_things(current_user)
    do
      # ...
    end
  end
end

The consensus was that yes, the schemas should not refer to each other, and to avoid this each context should have their OWN version of the schema with only the fields that context cares about, e.g. MyApp.MySchema.User and MyApp.MyOtherSchema.User. Underneath they can refer to the same table in your DB.

Right, so let's say we have an %App.Accounts.User{} we store in conn.assigns. Now, as we don't want to use schemas across contexts we define a App.Blog.User schema, which points to the same database table. The problem is that inside of our App.Blog context, we can't pattern match using %App.Blog.User{}, as the conn holds a %App.Accounts.User{}. The only way I see to do this is to break the rules or have a %App.User{}.

Maybe instead of a separate Policy module, the permission checks just live in the context itself. It's already a giant bag of functions, so might as well throw authorization on there.

That seems to make sense, as you'd only want to specify checks for functions of the api/context. Having a separate policy file would however make things a bit more structured, every context would then have a context/policy.ex file, which is nice, too.

I wonder if a @behaviour is most appropriate at that point. Specify authorize/3 as a behaviour and inject authorize!/3 and authorize?/3 wrappers for convenience.

Instead of magically unwrapping the assigns[:current_user] from a the conn, maybe provide a little macro to override the action/2:

That would certainly make the controllers nicer πŸ‘

Going with the behaviour approach for a moment, here's a fun idea: a first-class Bodyguard.Action struct to compose authorized actions, just like how Plug.Conn, or Ecto.Changeset works:

defmodule Bodyguard.Action do
  defstruct [
    context: nil,
    user: nil,
    auth_run?: false,
    auth_result: nil,
    authorized?: false,
    fallback: &(&1)
  ]
  # ...
end

import Bodyguard.Action
alias MyApp.MyContext

act(MyContext)                          # => %Bodyguard.Action{context: MyContext}
|> put_user(conn.assigns.current_user)  # => %{action | user: conn.assigns.current_user}
|> authorize(:list_users)               # uses MyContext.authorize/3 callback, sets auth_result, auth_run?, and authorized?
|> run(fn action ->                     # executes function if authorized, otherwise calls fallback(auth_result) 
  users = MyContext.list_users(action.user)
  render(conn, "index.html", users: users)
end)

Pros:

  • Don't have to use it – just use the Policy behaviour directly when that's all you need
  • Divorces Plug.Conn logic from actions
  • Can construct Actions ahead of time with known defaults, then just append what's needed per-action. For example, you could construct MyContext |> act() |> put_user(...) in a controller plug, then authorize-and-run in each action
  • A convenient extension point for future functionality

OK so I implemented the above interface on 2.0-dev – take a look. The docs are up-to-date, so mix docs to get the lay of the land.

Bodyguard.Policy is now a simple behaviour with one required callback, authorize/3. Given that assumption, now you can build a ton of functionality around it.

Bodyguard.Action is the new composable structure for incrementally building up an authorized action.

Per your feedback:

  • If you want a separate policy.ex file for structure, you can specify it explicitly using Bodyguard.Action.put_policy/2
  • You can grab the User for the appropriate context and assign it using Bodyguard.Action.put_user/2 – no dependency on the Plug.Conn
  • Controllers can be DRYer by building up the Action via plugs, then getting them back out via Bodyguard.Conn.get_action/2 or Bodyguard.Action.authorize_conn/3

Still thinking about how to best handle scoping. Contemplating a Bodyguard.Schema or Bodyguard.Scope behaviour and maybe some simple helpers around it, to be used within context functions.

@zimt28 Take a look at the latest on 2.0-dev. I think you'll find it meshes nicely with Phoenix 1.3 contexts and is flexible enough to support the different use cases we discussed above.

The API is almost totally different, so refactoring existing policies/scopes to support it will be a bit of work, but I think it's a net positive moving forward from 1.3.

I've redone my app in the 1.3 layout and used Bodyguard from this dev branch, – so far, so good over here.

Beautiful! I'll give it a try this weekend and let you know how it went, but I don't see any issues and love the simplicity :) Well done πŸ‘

I'd like to bring up a question I've had before again: Should authorization be an application or interface concern?

I think that everyone has to decide on his own, but what should be the suggested way? I will just use authorize! directly in my context functions, so authorization will be consistent across interfaces and I thinks it makes more sense, as the motivation behind 1.3 is to make clear that "Phoenix is not your application". What do you think?

Should authorization be an application or interface concern?

Since you brought it up, I'm honestly on the fence about this. All the examples I wrote for the docs show auth checks on the interface (e.g. controller actions) which is nice because failure handling is basically an interface concern, so it's easier to follow the "flow" of execution when it stays in the same layer.

There is the benefit of not having to pass around user everywhere in the contexts, although you will probably be passing in the user anyway to do something in relation to it, so that is a weak argument (ha!).

It could make testing a tad cleaner because you can test the raw context functions and their side effects independently of the auth checks – although the argument could be made that this is a Bad Idea because of that separation of logic.

I will just use authorize! directly in my context functions, so authorization will be consistent across interfaces and I thinks it makes more sense

This is a really good argument and I will definitely give it a shot when I start converting things over to contexts as well. Once you do the action/2 override in authenticated controllers, it becomes a lot nicer to work with user on the interface.

Although I would highly recommend looking at the authorize/3 form instead, using with clauses within your context functions. Gracefully handling the {:error, :unauthorized} result with a case or a fallback controller is really nice.

In the end, putting the auth checks directly IN the context works for both methods because you can utilize it within the context, or externally – it's ultimately up to you. I also added the policy option to use Bodyguard.Context if you wanted to specify a standalone policy module like how things worked in 1.0.

There is the benefit of not having to pass around user everywhere in the contexts, although you will probably be passing in the user anyway to do something in relation to it, so that is a weak argument (ha!).

For people using scopes (me πŸ˜„) this would probably always be the case.

All the examples I wrote for the docs show auth checks on the interface (e.g. controller actions) which is nice because failure handling is basically an interface concern, so it's easier to follow the "flow" of execution when it stays in the same layer.

Although I would highly recommend looking at the authorize/3 form instead, using with clauses within your context functions. Gracefully handling the {:error, :unauthorized} result with a case or a fallback controller is really nice.

I'm not sure how I'll do this. I'd like to perform the checks inside my context's functions, but I'm not sure whether authorize/3 or authorize!/3 is the way to go. When using my app with a Phoenix interface, authorize!/3 would just throw a Bodyguard.NotAuthorizedError which can be handled properly, but in order to be interface agnostic, authorize/3's output would be nicer - this however would require checks (with) in the controllers again.

this however would require checks (with) in the controllers again.

Could you just put the with in the context function? It would be an extra level of indentation but you could do it.

def MyApp.Blog do
  def create_post(user, params) do
    with :ok <- authorize(:create_post, user) do
      # ...
    end
  end
end

So then Blog.create_post/2 could return {:ok, post} or {:error, :unauthorized}

That's what I want to do, but then I'd have to use with syntax in the contexts and controllers, otherwise pattern matching in the controllers might fail:

def MyApp.Web.PostController do
  def create(conn, %{"post" => post_params}, _user) do
    user = nil
    post = MyApp.Blog.create_post(user, user_params)
  end
end
** (MatchError) no match of right hand side value: {:error, :unauthorized}

What I'm thinking about is the following, this would allow to skip with in the controllers, leading to much cleaner and less repetitive code:

defimpl Plug.Exception, for: MatchError do
  @unauthorized {:error, :unauthorized}

  def status(%{term: @unauthorized}), do: 403
  def status(_), do: 500
end

Yup, that could work. It's totally up to you – it's a tradeoff of explicitness.

it's a tradeoff of explicitness.

That's my biggest concern yet. The other way I see is to wrap the authorize calls in Auth.check or something, which then calls authorize/3 or authorize!/3, depending on how the inferface has configured it to work.

I think I'll try the approach mentioned above first and then see if it feels right :)

Great. I'm going to wait until the official 1.3.0 release before bumping Bodyguard to 2.0, just in case there are any changes to Phoenix that would warrant a re-think of Bodyguard. But that doesn't seem likely, so I expect the Bodyguard API to be stable.

It's a really helpful discussion! I think authorization with context API is the right direction. I have a question though:

I see the authorize/3's last parameter is a params, does that mean I need to pass it even for creation logic? like authorize(:create_post, user, nil) ? Can the API to treat the last parameter as optional? Not sure if callback can do it though

@darkbaby123 Since you're defining the authorize/3 callback, it's up to you to provide default values for params in the callback definition. All the Bodyguard helpers (authorize!/3, authorize?/3, etc) default it to %{}, so you should do something like def authorize(action, user, params \\ %{})

@schrockwell Thanks! I borrowed part of the policy code from 2.0-dev to my own project. And I choose to add default parameter to authorize!/3. I'm really looking forward 1.3 to come out then I can switch to Bodyguard 2.0.

@zimt28 I'll leave my 2 cents to call the authorize in web interface instead of application interface. Because there're some of APIs doesn't need user. If I wrote authorize in my context then I have to add user parameter to every context API. It makes the context API less reusable. One example is deleting:

with {:ok, post} = Blog.get_post(user, id),
    :ok <- Blog.authorize(:delete_post, user, %{post: post}),
    {:ok, _} <- Blog.delete_post(post)  # don't need user here
do
  # render something
end

@schrockwell

Instead of magically unwrapping the assigns[:current_user] from a the conn, maybe provide a little macro to override the action/2:

Typically, I do this by injecting the current_user (or any current resource) via Plug then overriding the controller action/2 to pull the current_user from conn.assigns and injecting it in the action args.

  pipeline :browser do
    # ...
    plug Guardian.Plug.VerifySession
    plug Guardian.Plug.LoadResource

    plug MyApp.Plug.CurrentAccount
  end
defmodule MyApp.Plug.CurrentAccount do
  @moduledoc """
  Loads the current account into assigns from Guardian.
  """

  def init(opts), do: opts

  def call(conn, _opts) do
    Plug.Conn.assign(conn, :current_account, current_account(conn))
  end

  defp current_account(conn) do
    Guardian.Plug.current_resource(conn)
  end
end

Thanks for the helpful discussion.

Hi folks, just a quick update after the above discussions. I shuffled around the API to make things more concise. At a high level, the new Bodyguard.permit/4 and Bodyguard.scope/3 helpers are now the entry points into the API, instead of calling the callbacks directly.

If you've been using 2.0-dev, here's a summary of changes:

  • Behaviours and callbacks have not changed at all
  • Bodyguard.Policy.authorize/4 helper and friends moved to Bodyguard.permit/4 (name change! Callback is still authorize)
  • Bodyguard.Context has been removed
  • Bodyguard.Schema.scope/3 helper moved to Bodyguard.scope/3
  • In contexts, replace use Bodyguard.Context with @behaviour Bodyguard.Policy; import Bodyguard
  • In schemas, replace use Bodyguard.Schema with @behaviour Bodyguard.Schema
  • When doing auth checks, replace MyContext.authorize(action, user, %{param: value}) with Bodyguard.permit(MyContext, action, user, param: value) (same for bang and boolean variations)
  • When doing scoping, replace MyContext.MySchema.scope(query, user, %{param: value}) with Bodyguard.scope(query, user, param: value)
  • params are now always keyword lists for callers, and converted to maps for callbacks (thanks for the insight, @darkbaby123)

Look at the diffs for the tests helpers and test cases to see some examples. The docs and readme have been updated too, so mix docs for the latest.

I apologize for making API changes this late in the game. I wouldn't do it if I didn't think it was actively improving the library and making it more flexible for the future. Thanks for understanding.

I'm planning on using 2.0-dev (or its respective release, once that's out) with Phoenix 1.3-rc2. Just wanted to check back and ask, what's the status of the refactoring?

@cybrox No API changes planned - go for it!

Wow, that was fast! Thanks, I'll give it a shot! πŸ˜„

Hi everyone – v2.0.0 is live on Hex now! Thanks for all the constructive discussions.