项目作者: schrockwell

项目描述 :
Simple authorization conventions for Phoenix apps
高级语言: Elixir
项目地址: git://github.com/schrockwell/bodyguard.git
创建时间: 2016-08-26T04:50:05Z
项目社区:https://github.com/schrockwell/bodyguard

开源协议:MIT License

下载


Bodyguard

Module Version
Hex Docs
Total Download
License
Last Updated
tests

Bodyguard protects the context boundaries of your application. 💪

Authorization callbacks are implemented directly on context modules, so permissions can be checked from controllers, views, sockets, tests, and even other contexts.

The Bodyguard.Policy behaviour has a single required callback, c:Bodyguard.Policy.authorize/3. Additionally, the Bodyguard.Schema behaviour provides a convention for limiting query results per-user.

Quick Example

Define authorization rules directly in the context module:

  1. # lib/my_app/blog/blog.ex
  2. defmodule MyApp.Blog do
  3. @behaviour Bodyguard.Policy
  4. # Admins can update anything
  5. def authorize(:update_post, %{role: :admin} = _user, _post), do: :ok
  6. # Users can update their owned posts
  7. def authorize(:update_post, %{id: user_id} = _user, %{user_id: user_id} = _post), do: :ok
  8. # Otherwise, denied
  9. def authorize(:update_post, _user, _post), do: :error
  10. end
  11. # lib/my_app_web/controllers/post_controller.ex
  12. defmodule MyAppWeb.PostController do
  13. use MyAppWeb, :controller
  14. def update(conn, %{"id" => id, "post" => post_params}) do
  15. user = conn.assigns.current_user
  16. post = MyApp.Blog.get_post!(id)
  17. with :ok <- Bodyguard.permit(MyApp.Blog, :update_post, user, post),
  18. {:ok, post} <- MyApp.Blog.update_post(post, post_params)
  19. do
  20. redirect(conn, to: post_path(conn, :show, post))
  21. end
  22. end
  23. end

Policies

To implement a policy, add @behaviour Bodyguard.Policy to a context, then define an authorize(action, user, params) callback, which must return:

  • :ok or true to permit an action
  • :error, {:error, reason}, or false to deny an action

Don’t use these callbacks directly - instead, go through Bodyguard.permit/4. This will convert keyword-list params into a map, and will coerce the callback result into a strict :ok or {:error, reason} result. The default failure result is {:error, :unauthorized}.

Helpers Bodyguard.permit?/4 and Bodyguard.permit!/5 are also provided.

  1. # lib/my_app/blog/blog.ex
  2. defmodule MyApp.Blog do
  3. @behaviour Bodyguard.Policy
  4. alias __MODULE__
  5. # Admin users can do anything
  6. def authorize(_, %Blog.User{role: :admin}, _), do: true
  7. # Regular users can create posts
  8. def authorize(:create_post, _, _), do: true
  9. # Regular users can modify their own posts
  10. def authorize(action, %Blog.User{id: user_id}, %Blog.Post{user_id: user_id})
  11. when action in [:update_post, :delete_post], do: true
  12. # Catch-all: deny everything else
  13. def authorize(_, _, _), do: false
  14. end

If you want to keep the policy separate from the context, define a dedicated policy module and use defdelegate:

  1. # lib/my_app/blog/blog.ex
  2. defmodule MyApp.Blog do
  3. defdelegate authorize(action, user, params), to: MyApp.Blog.Policy
  4. end
  5. # lib/my_app/blog/policy.ex
  6. defmodule MyApp.Blog.Policy do
  7. @behaviour Bodyguard.Policy
  8. def authorize(action, user, params), do: # ...
  9. end

Controllers

The action_fallback controller macro is the recommended way to deal with authorization failures. The fallback controller will handle the {:error, reason} results from the main controllers.

  1. # lib/my_app_web/controllers/fallback_controller.ex
  2. defmodule MyAppWeb.FallbackController do
  3. use MyAppWeb, :controller
  4. def call(conn, {:error, :unauthorized}) do
  5. conn
  6. |> put_status(:forbidden)
  7. |> put_view(html: MyAppWeb.ErrorHTML)
  8. |> render(:"403")
  9. end
  10. end
  11. # lib/my_app_controllers/page_controller.ex
  12. defmodule MyAppWeb.PageController do
  13. use MyAppWeb, :controller
  14. # This can be defined here, or in the MyAppWeb.controller/0 macro
  15. action_fallback MyAppWeb.FallbackController
  16. # ...actions here...
  17. end

When Using the Plug

If the Bodyguard.Plug.Authorize plug is being used, its :fallback option must be specified, since the plug pipeline will be halted before the controller action can be called.

Returning “404 Not Found”

Typically, failures will result in {:error, :unauthorized}. If you wish to deny access without leaking the existence of a particular resource, consider returning {:error, :not_found} instead, and handle it separately in the fallback controller as a 404.

Bodyguard doesn’t make any assumptions about where authorization checks are performed. You can do it before calling into the context, or within the context itself. There is a good discussion of the tradeoffs in this blog post.

See the section “Overriding action/2 for custom arguments” in the Phoenix.Controller docs for a clean way to pass in the user to each action.

Plugs

  • Bodyguard.Plug.Authorize – perform authorization in the middle of a pipeline

This plug’s config utilizes callback functions called getters, which are 1-arity functions that
accept the conn and return the appropriate value.

  1. # lib/my_app_web/controllers/post_controller.ex
  2. defmodule MyAppWeb.PostController do
  3. use MyAppWeb, :controller
  4. # Fetch the post and put into conn assigns
  5. plug :get_post when action in [:show]
  6. # Do the check
  7. plug Bodyguard.Plug.Authorize,
  8. policy: MyApp.Blog.Policy,
  9. action: {Phoenix.Controller, :action_name},
  10. user: {MyApp.Authentication, :current_user},
  11. params: {__MODULE__, :extract_post},
  12. fallback: MyAppWeb.FallbackController
  13. def show(conn, _) do
  14. # Already assigned and authorized
  15. render(conn, "show.html")
  16. end
  17. defp get_post(conn, _) do
  18. assign(conn, :post, MyApp.Posts.get_post!(conn.params["id"]))
  19. end
  20. # Helper for the Authorize plug
  21. def extract_post(conn), do: conn.assigns.posts
  22. end

See the docs for more information about configuring application-wide defaults for the plug.

LiveViews

Authorization checks can be performed in the mount/3 and handle_event/3 callbacks of a LiveView. See the LiveView documentation for hints and examples.

Schema Scopes

Bodyguard also provides the Bodyguard.Schema behaviour to query which items a user can access. Implement it directly on schema modules.

  1. # lib/my_app/blog/post.ex
  2. defmodule MyApp.Blog.Post do
  3. import Ecto.Query, only: [from: 2]
  4. @behaviour Bodyguard.Schema
  5. def scope(query, %MyApp.Blog.User{id: user_id}, _) do
  6. from ms in query, where: ms.user_id == ^user_id
  7. end
  8. end

To leverage scopes, the Bodyguard.scope/4 helper function (not the callback!) can infer the type of a query and automatically defer to the appropriate callback.

  1. # lib/my_app/blog/blog.ex
  2. defmodule MyApp.Blog do
  3. def list_user_posts(user) do
  4. MyApp.Blog.Post
  5. |> Bodyguard.scope(user) # <-- defers to MyApp.Blog.Post.scope/3
  6. |> where(draft: false)
  7. |> Repo.all
  8. end
  9. end

Configuration

Here is the default library config.

  1. config :bodyguard,
  2. # The second element of the {:error, reason} tuple returned on auth failure
  3. default_error: :unauthorized

Testing

Testing is pretty straightforward – use the Bodyguard top-level API.

  1. assert :ok == Bodyguard.permit(MyApp.Blog, :successful_action, user)
  2. assert {:error, :unauthorized} == Bodyguard.permit(MyApp.Blog, :failing_action, user)
  3. assert Bodyguard.permit?(MyApp.Blog, :successful_action, user)
  4. refute Bodyguard.permit?(MyApp.Blog, :failing_action, user)
  5. error = assert_raise Bodyguard.NotAuthorizedError, fun ->
  6. Bodyguard.permit(MyApp.Blog, :failing_action, user)
  7. end
  8. assert %{status: 403, message: "not authorized"} = error

Installation

  1. Add :bodyguard to your list of dependencies:

    1. # mix.exs
    2. def deps do
    3. [
    4. {:bodyguard, "~> 2.4"}
    5. ]
    6. end
  2. Add @behaviour Bodyguard.Policy to contexts that require authorization, and implement c:Bodyguard.Policy.authorize/3 callbacks.

  3. Create up a fallback controller to render an error on {:error, :unauthorized}.

Optional Installation Steps

  1. Add @behaviour Bodyguard.Schema on schemas available for user-scoping, and implement c:Bodyguard.Schema.scope/3 callbacks.

  2. Edit my_app_web.ex and add import Bodyguard to controllers, views, channels, etc.

Alternatives

Not what you’re looking for?

Community

Join our communities!

License

MIT License, Copyright (c) 2024 Rockwell Schrock