{"id":13507460,"url":"https://github.com/schrockwell/bodyguard","last_synced_at":"2025-05-14T19:07:55.076Z","repository":{"id":10682920,"uuid":"66617472","full_name":"schrockwell/bodyguard","owner":"schrockwell","description":"Simple authorization conventions for Phoenix apps","archived":false,"fork":false,"pushed_at":"2024-05-24T21:00:58.000Z","size":238,"stargazers_count":765,"open_issues_count":1,"forks_count":48,"subscribers_count":17,"default_branch":"main","last_synced_at":"2025-03-30T08:32:17.032Z","etag":null,"topics":["authorization","elixir","phoenix"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/bodyguard/","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/schrockwell.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-08-26T04:50:05.000Z","updated_at":"2025-03-28T10:49:13.000Z","dependencies_parsed_at":"2024-05-01T15:27:40.469Z","dependency_job_id":"bffab77b-8c1c-4e06-9498-e026661c4e7b","html_url":"https://github.com/schrockwell/bodyguard","commit_stats":{"total_commits":154,"total_committers":27,"mean_commits":5.703703703703703,"dds":"0.24675324675324672","last_synced_commit":"849369972710616c0a4cd1d0f277bf8b0a9affab"},"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schrockwell%2Fbodyguard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schrockwell%2Fbodyguard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schrockwell%2Fbodyguard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/schrockwell%2Fbodyguard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/schrockwell","download_url":"https://codeload.github.com/schrockwell/bodyguard/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248091222,"owners_count":21046236,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authorization","elixir","phoenix"],"created_at":"2024-08-01T02:00:34.182Z","updated_at":"2025-04-09T18:46:26.016Z","avatar_url":"https://github.com/schrockwell.png","language":"Elixir","funding_links":[],"categories":["Authorization"],"sub_categories":[],"readme":"# Bodyguard\n\n[![Module Version](https://img.shields.io/hexpm/v/bodyguard.svg)](https://hex.pm/packages/bodyguard)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/bodyguard/)\n[![Total Download](https://img.shields.io/hexpm/dt/bodyguard.svg)](https://hex.pm/packages/bodyguard)\n[![License](https://img.shields.io/hexpm/l/bodyguard.svg)](https://github.com/schrockwell/bodyguard/blob/master/LICENSE)\n[![Last Updated](https://img.shields.io/github/last-commit/schrockwell/bodyguard.svg)](https://github.com/schrockwell/bodyguard/commits/master)\n[![tests](https://github.com/schrockwell/bodyguard/actions/workflows/tests-v2.yml/badge.svg)](https://github.com/schrockwell/bodyguard/actions)\n\nBodyguard protects the context boundaries of your application. 💪\n\nAuthorization callbacks are implemented directly on context modules, so permissions can be checked from controllers, views, sockets, tests, and even other contexts.\n\nThe `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.\n\n- [Docs](https://hexdocs.pm/bodyguard/) ← complete documentation\n- [Hex](https://hex.pm/packages/bodyguard)\n- [GitHub](https://github.com/schrockwell/bodyguard)\n\n## Quick Example\n\nDefine authorization rules directly in the context module:\n\n```elixir\n# lib/my_app/blog/blog.ex\ndefmodule MyApp.Blog do\n  @behaviour Bodyguard.Policy\n\n  # Admins can update anything\n  def authorize(:update_post, %{role: :admin} = _user, _post), do: :ok\n\n  # Users can update their owned posts\n  def authorize(:update_post, %{id: user_id} = _user, %{user_id: user_id} = _post), do: :ok\n\n  # Otherwise, denied\n  def authorize(:update_post, _user, _post), do: :error\nend\n\n# lib/my_app_web/controllers/post_controller.ex\ndefmodule MyAppWeb.PostController do\n  use MyAppWeb, :controller\n\n  def update(conn, %{\"id\" =\u003e id, \"post\" =\u003e post_params}) do\n    user = conn.assigns.current_user\n    post = MyApp.Blog.get_post!(id)\n\n    with :ok \u003c- Bodyguard.permit(MyApp.Blog, :update_post, user, post),\n      {:ok, post} \u003c- MyApp.Blog.update_post(post, post_params)\n    do\n      redirect(conn, to: post_path(conn, :show, post))\n    end\n  end\nend\n```\n\n## Policies\n\nTo implement a policy, add `@behaviour Bodyguard.Policy` to a context, then define an `authorize(action, user, params)` callback, which must return:\n\n- `:ok` or `true` to permit an action\n- `:error`, `{:error, reason}`, or `false` to deny an action\n\nDon'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}`.\n\nHelpers `Bodyguard.permit?/4` and `Bodyguard.permit!/5` are also provided.\n\n```elixir\n# lib/my_app/blog/blog.ex\ndefmodule MyApp.Blog do\n  @behaviour Bodyguard.Policy\n  alias __MODULE__\n\n  # Admin users can do anything\n  def authorize(_, %Blog.User{role: :admin}, _), do: true\n\n  # Regular users can create posts\n  def authorize(:create_post, _, _), do: true\n\n  # Regular users can modify their own posts\n  def authorize(action, %Blog.User{id: user_id}, %Blog.Post{user_id: user_id})\n    when action in [:update_post, :delete_post], do: true\n\n  # Catch-all: deny everything else\n  def authorize(_, _, _), do: false\nend\n```\n\nIf you want to keep the policy separate from the context, define a dedicated policy module and use `defdelegate`:\n\n```elixir\n# lib/my_app/blog/blog.ex\ndefmodule MyApp.Blog do\n  defdelegate authorize(action, user, params), to: MyApp.Blog.Policy\nend\n\n# lib/my_app/blog/policy.ex\ndefmodule MyApp.Blog.Policy do\n  @behaviour Bodyguard.Policy\n\n  def authorize(action, user, params), do: # ...\nend\n```\n\n## Controllers\n\nThe `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.\n\n```elixir\n# lib/my_app_web/controllers/fallback_controller.ex\ndefmodule MyAppWeb.FallbackController do\n  use MyAppWeb, :controller\n\n  def call(conn, {:error, :unauthorized}) do\n    conn\n    |\u003e put_status(:forbidden)\n    |\u003e put_view(html: MyAppWeb.ErrorHTML)\n    |\u003e render(:\"403\")\n  end\nend\n\n# lib/my_app_controllers/page_controller.ex\ndefmodule MyAppWeb.PageController do\n  use MyAppWeb, :controller\n\n  # This can be defined here, or in the MyAppWeb.controller/0 macro\n  action_fallback MyAppWeb.FallbackController\n\n  # ...actions here...\nend\n```\n\n### When Using the Plug\n\nIf 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.\n\n### Returning \"404 Not Found\"\n\nTypically, 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.\n\n### Related Reading\n\nBodyguard 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](https://dockyard.com/blog/2017/08/01/authorization-for-phoenix-contexts).\n\nSee the section \"Overriding `action/2` for custom arguments\" in [the Phoenix.Controller docs](https://hexdocs.pm/phoenix/Phoenix.Controller.html) for a clean way to pass in the `user` to each action.\n\n## Plugs\n\n- `Bodyguard.Plug.Authorize` – perform authorization in the middle of a pipeline\n\nThis plug's config utilizes callback functions called getters, which are 1-arity functions that\naccept the `conn` and return the appropriate value.\n\n```elixir\n# lib/my_app_web/controllers/post_controller.ex\ndefmodule MyAppWeb.PostController do\n  use MyAppWeb, :controller\n\n  # Fetch the post and put into conn assigns\n  plug :get_post when action in [:show]\n\n  # Do the check\n  plug Bodyguard.Plug.Authorize,\n    policy: MyApp.Blog.Policy,\n    action: {Phoenix.Controller, :action_name},\n    user: {MyApp.Authentication, :current_user},\n    params: {__MODULE__, :extract_post},\n    fallback: MyAppWeb.FallbackController\n\n  def show(conn, _) do\n    # Already assigned and authorized\n    render(conn, \"show.html\")\n  end\n\n  defp get_post(conn, _) do\n    assign(conn, :post, MyApp.Posts.get_post!(conn.params[\"id\"]))\n  end\n\n  # Helper for the Authorize plug\n  def extract_post(conn), do: conn.assigns.posts\nend\n```\n\nSee the docs for more information about configuring application-wide defaults for the plug.\n\n## LiveViews\n\nAuthorization checks can be performed in the `mount/3` and `handle_event/3` callbacks of a LiveView. See [the LiveView documentation](https://hexdocs.pm/phoenix_live_view/security-model.html) for hints and examples.\n\n## Schema Scopes\n\nBodyguard also provides the `Bodyguard.Schema` behaviour to query which items a user can access. Implement it directly on schema modules.\n\n```elixir\n# lib/my_app/blog/post.ex\ndefmodule MyApp.Blog.Post do\n  import Ecto.Query, only: [from: 2]\n  @behaviour Bodyguard.Schema\n\n  def scope(query, %MyApp.Blog.User{id: user_id}, _) do\n    from ms in query, where: ms.user_id == ^user_id\n  end\nend\n```\n\nTo 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.\n\n```elixir\n# lib/my_app/blog/blog.ex\ndefmodule MyApp.Blog do\n  def list_user_posts(user) do\n    MyApp.Blog.Post\n    |\u003e Bodyguard.scope(user) # \u003c-- defers to MyApp.Blog.Post.scope/3\n    |\u003e where(draft: false)\n    |\u003e Repo.all\n  end\nend\n```\n\n## Configuration\n\nHere is the default library config.\n\n```elixir\nconfig :bodyguard,\n  # The second element of the {:error, reason} tuple returned on auth failure\n  default_error: :unauthorized\n```\n\n## Testing\n\nTesting is pretty straightforward – use the `Bodyguard` top-level API.\n\n```elixir\nassert :ok == Bodyguard.permit(MyApp.Blog, :successful_action, user)\nassert {:error, :unauthorized} == Bodyguard.permit(MyApp.Blog, :failing_action, user)\n\nassert Bodyguard.permit?(MyApp.Blog, :successful_action, user)\nrefute Bodyguard.permit?(MyApp.Blog, :failing_action, user)\n\nerror = assert_raise Bodyguard.NotAuthorizedError, fun -\u003e\n  Bodyguard.permit(MyApp.Blog, :failing_action, user)\nend\nassert %{status: 403, message: \"not authorized\"} = error\n```\n\n## Installation\n\n1.  Add `:bodyguard` to your list of dependencies:\n\n    ```elixir\n    # mix.exs\n    def deps do\n      [\n        {:bodyguard, \"~\u003e 2.4\"}\n      ]\n    end\n    ```\n\n2.  Add `@behaviour Bodyguard.Policy` to contexts that require authorization, and implement `c:Bodyguard.Policy.authorize/3` callbacks.\n\n3.  Create up a [fallback controller](#controllers) to render an error on `{:error, :unauthorized}`.\n\n### Optional Installation Steps\n\n1.  Add `@behaviour Bodyguard.Schema` on schemas available for user-scoping, and implement `c:Bodyguard.Schema.scope/3` callbacks.\n\n2.  Edit `my_app_web.ex` and add `import Bodyguard` to controllers, views, channels, etc.\n\n## Alternatives\n\nNot what you're looking for?\n\n- [Roll your own](https://dockyard.com/blog/2017/08/01/authorization-for-phoenix-contexts)\n- [PolicyWonk](https://github.com/boydm/policy_wonk)\n- [Canada](https://github.com/jarednorman/canada)\n- [Canary](https://github.com/cpjk/canary)\n\n## Community\n\nJoin our communities!\n\n- [Slack](https://elixir-lang.slack.com/messages/CHMTNPSEN/)\n\n## License\n\nMIT License, Copyright (c) 2024 Rockwell Schrock\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fschrockwell%2Fbodyguard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fschrockwell%2Fbodyguard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fschrockwell%2Fbodyguard/lists"}