{"id":13507462,"url":"https://github.com/cpjk/canary","last_synced_at":"2026-02-22T11:05:45.199Z","repository":{"id":30813425,"uuid":"34370594","full_name":"cpjk/canary","owner":"cpjk","description":":hatching_chick: Elixir authorization and resource-loading library for Plug applications.","archived":false,"fork":false,"pushed_at":"2025-02-11T16:56:57.000Z","size":206,"stargazers_count":472,"open_issues_count":6,"forks_count":51,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-02-11T17:42:34.706Z","etag":null,"topics":["authorization","elixir","phoenix-application","phoenix-framework","plug"],"latest_commit_sha":null,"homepage":"","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/cpjk.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":"2015-04-22T05:23:42.000Z","updated_at":"2025-02-11T16:57:01.000Z","dependencies_parsed_at":"2024-05-01T15:27:45.669Z","dependency_job_id":"c7ef8875-bc50-419c-85a1-93d63ac8ea3b","html_url":"https://github.com/cpjk/canary","commit_stats":{"total_commits":213,"total_committers":16,"mean_commits":13.3125,"dds":0.2863849765258216,"last_synced_commit":"ae03a75d0d02e4fe418da53ba5f97faac31a0917"},"previous_names":[],"tags_count":27,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cpjk%2Fcanary","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cpjk%2Fcanary/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cpjk%2Fcanary/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cpjk%2Fcanary/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cpjk","download_url":"https://codeload.github.com/cpjk/canary/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246296364,"owners_count":20754625,"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-application","phoenix-framework","plug"],"created_at":"2024-08-01T02:00:34.239Z","updated_at":"2025-10-21T18:45:18.198Z","avatar_url":"https://github.com/cpjk.png","language":"Elixir","funding_links":[],"categories":["Authorization"],"sub_categories":[],"readme":"Canary\n======\n[![Actions Status](https://github.com/cpjk/canary/workflows/CI/badge.svg)](https://github.com/runhyve/canary/actions?query=workflow%3ACI)\n[![Hex pm](https://img.shields.io/hexpm/v/canary.svg?style=flat)](https://hex.pm/packages/canary)\n\nAn authorization library in Elixir for `Plug` and `Phoenix.LiveView` applications that restricts what resources the current user is allowed to access, and automatically load and assigns resources.\n\nInspired by [CanCan](https://github.com/CanCanCommunity/cancancan) for Ruby on Rails.\n\n[Read the docs](https://hexdocs.pm/canary/2.0.0-dev/getting-started.html)\n\n# Canary 2.0.0\n\nThe `master` branch is for the development of Canary 2.0.0. Check out [branch 1.2.x](https://github.com/cpjk/canary/tree/1.2.x) if you are looking Canary 1 (only plug authentication).\n\n## Installation\n\nFor the latest master (2.0.0-dev):\n\n```elixir\ndefp deps do\n  {:canary, github: \"cpjk/canary\"}\nend\n```\n\nFor the latest release:\n\n```elixir\ndefp deps do\n  {:canary, \"~\u003e 2.0.0-dev\"}\nend\n```\n\nThen run `mix deps.get` to fetch the dependencies.\n\n## Quick start\n\nCanary provides functions to be used as plugs or LiveView hooks to load and authorize resources:\n\n`load_resource`, `authorize_resource`, `authorize_controller`*, and `load_and_authorize_resource`.\n\n`load_resource` and `authorize_resource` can be used by themselves, while `load_and_authorize_resource` combines them both.\n\n*Available only in plug based authentication*\n\nIn order to use Canary, you will need, at minimum:\n\n- A [Canada.Can protocol](https://github.com/jarednorman/canada) implementation (a good place would be `lib/abilities.ex`)\n\n- An Ecto record struct containing the user to authorize in `assigns.current_user` (the key can be customized - [see more](#overriding-the-default-user)).\n\n- Your Ecto repo specified in your `config/config.exs`: `config :canary, repo: YourApp.Repo`\n\nFor the plugs just `import Canary.Plugs`. In a Phoenix app the best place would probably be inside `controller/0` in your `web/web.ex`, in order to make the functions available in all of your controllers.\n\nFor the liveview hooks just `use Canary.Hooks`. In a Phoenix app the best place would probably be inside `live_view/0` in your `web/web.ex`, in order to make the functions available in all of your controllers.\n\n\n### load_resource\n\nLoads the resource having the id given in `params[\"id\"]` from the database using the given Ecto repo and model, and assigns the resource to `assigns.\u003cresource_name\u003e`, where `resource_name` is inferred from the model name.\n\n\u003c!-- tabs-open --\u003e\n### Conn Plugs example\n```elixir\nplug :load_resource, model: Project.Post\n```\n\nWill load the `Project.Post` having the id given in `conn.params[\"id\"]` through `YourApp.Repo`, and assign it to `conn.assigns.post`.\n\n### LiveView Hooks example\n```elixir\nmount_canary :load_resource, model: Project.Post\n```\n\nWill load the `Project.Post` having the id given in `params[\"id\"]` through `YourApp.Repo`, and assign it to `socket.assigns.post`\n\u003c!-- tabs-close --\u003e\n\n### authorize_resource\n\nChecks whether or not the `current_user` for the request can perform the given action on the given resource and assigns the result (true/false) to `assigns.authorized`. It is up to you to decide what to do with the result.\n\nFor Phoenix applications, Canary determines the action automatically.\nFor non-Phoenix applications, or to override the action provided by Phoenix, simply ensure that `assigns.canary_action` contains an atom specifying the action.\n\nFor the LiveView on `handle_params` it uses `socket.assigns.live_action` as action, on `handle_event` it uses the event name as action.\n\n\n\nIn order to authorize resources, you must specify permissions by implementing the [Canada.Can protocol](https://github.com/jarednorman/canada) for your `User` model (Canada is included as a light weight dependency).\n\n### load_and_authorize_resource\n\nAuthorizes the resource and then loads it if authorization succeeds. Again, the resource is loaded into `assigns.\u003cresource_name\u003e`.\n\nIn the following example, the `Post` with the same `user_id` as the `current_user` is only loaded if authorization succeeds.\n\n## Usage Example\n\nLet's say you have a Phoenix application with a `Post` model, and you want to authorize the `current_user` for accessing `Post` resources.\n\nLet's suppose that you have a file named `lib/abilities.ex` that contains your Canada authorization rules like so:\n\n```elixir\ndefimpl Canada.Can, for: User do\n  def can?(%User{ id: user_id }, action, %Post{ user_id: user_id })\n    when action in [:show], do: true\n\n  def can?(%User{ id: user_id }, _, _), do: false\nend\n```\n\n### Example for Conn Plugs\n\nIn your `web/router.ex:` you have:\n\n```elixir\nget \"/posts/:id\", PostController, :show\ndelete \"/posts/:id\", PostController, :delete\n```\n\nTo automatically load and authorize on the `Post` having the `id` given in the params, you would add the following plug to your `PostController`:\n\n```elixir\nplug :load_and_authorize_resource, model: Post\n```\n\nIn this case, on `GET /posts/12` authorization succeeds, and the `Post` specified by `conn.params[\"id]` will be loaded into `conn.assigns.post`.\n\nHowever, on `DELETE /posts/12`, authorization fails and the `Post` resource is not loaded.\n\n### Example for LiveView Hooks\n\nIn your `web/router.ex:` you have:\n\n```elixir\nlive \"/posts/:id\", PostLive, :show\n```\n\nand in your PostLive module `web/live/post_live.ex`:\n\n```elixir\ndefmodule MyAppWeb.PostLive do\n  use MyAppWeb, :live_view\n\n  def render(assigns) do\n    ~H\"\"\"\n    Post id: {@post.id}\n    \u003cbutton phx-click=\"delete\"\u003eDelete\u003c/button\u003e\n    \"\"\"\n  end\n\n  def mount(_params, _session, socket), do: {:ok, socket}\n\n  def handle_event(\"delete\", _params, socket) do\n    # Do the action\n    {:noreply, update(socket, :temperature, \u0026(\u00261 + 1))}\n  end\nend\n```\n\nTo automatically load and authorize on the `Post` having the `id` given in the params, you would add the following hook to your `PostLive`:\n\n```elixir\nmount_hook :load_and_authorize_resource, model: Post\n```\n\nIn this case, once opening `/posts/12` the `load_and_authorize_resource` on `handle_params` stage will be performed. The the `Post` specified by `params[\"id]` will be loaded into `socket.assigns.post`.\n\nHowever, when the `delete` event will be triggered, authorization fails and the `Post` resource is not loaded. Socket will be halted.\n\n### Excluding actions\n\nTo exclude an action from any of the plugs, pass the `:except` key, with a single action or list of actions.\n\nFor example,\n\nSingle action form:\n\n```elixir\nplug :load_and_authorize_resource, model: Post, except: :show\n\nmount_canary :load_and_authorize_resource, model: Post, except: :show\n```\n\nList form:\n\n```elixir\nplug :load_and_authorize_resource, model: Post, except: [:show, :create]\n\nmount_canary :load_and_authorize_resource, model: Post, except: [:show, :create]\n```\n\n### Authorizing only specific actions\n\nTo specify that a plug should be run only for a specific list of actions, pass the `:only` key, with a single action or list of actions.\n\nFor example,\n\nSingle action form:\n\n```elixir\nplug :load_and_authorize_resource, model: Post, only: :show\n\nmount_canary :load_and_authorize_resource, model: Post, only: :show\n```\n\nList form:\n\n```elixir\nplug :load_and_authorize_resource, model: Post, only: [:show, :create]\n\nmount_canary :load_and_authorize_resource, model: Post, only: [:show, :create]\n```\n\n\u003e Note: Having both `:only` and `:except` in opts is invalid. Canary will raise `ArgumentError` \"You can't use both :except and :only options\"\n\n### Overriding the default user\n\nGlobally, the default key for finding the user to authorize can be set in your configuration as follows:\n\n```elixir\nconfig :canary, current_user: :some_current_user\n```\n\nIn this case, canary will look for the current user record in `assigns.some_current_user`.\n\nThe current user key can also be overridden for individual plugs as follows:\n\n```elixir\nplug :load_and_authorize_resource, model: Post, current_user: :current_admin\n\nmount_canary :load_and_authorize_resource, model: Post, current_user: :current_admin\n```\n\n### Specifying resource_name\n\nTo specify the name under which the loaded resource is stored, pass the `:as` flag in the plug declaration.\n\nFor example,\n\n```elixir\nplug :load_and_authorize_resource, model: Post, as: :new_post\n\nmount_canary :load_and_authorize_resource, model: Post, as: :new_post\n```\n\nwill load the post into `assigns.new_post`\n\n### Preloading associations\n\nAssociations can be preloaded with `Repo.preload` by passing the `:preload` option with the name of the association:\n\n```elixir\nplug :load_and_authorize_resource, model: Post, preload: :comments\n\nmount_canary :load_and_authorize_resource, model: Post, preload: :comments\n```\n\n### Non-id actions\n\nTo authorize actions where there is no loaded resource, the resource passed to the `Canada.Can` implementation should be the module name of the model rather than a struct.\n\nTo authorize such actions use `authorize_resource` plug with `required: false` option\n\n```elixir\nplug :authorize_resource, model: Post, only: [:index, :new, :create], required: false\n\nmount_canary :authorize_resource, model: Post, only: [:index, :new, :create], required: false\n```\n\nFor example, when authorizing access to the `Post` resource, you should use\n\n```elixir\ndef can?(%User{}, :index, Post), do: true\n```\n\ninstead of\n\n```elixir\ndef can?(%User{}, :index, %Post{}), do: true\n```\n\n\u003e ### Deprecated {: .warning}\n\u003e\n\u003e The `:non_id_actions` is deprecated as of 2.0.0-dev and will be removed in Canary 2.1.0\n\u003e Please follow the [Upgrade guide to 2.0.0](docs/upgrade.md#upgrading-from-canary-1-2-0-to-2-0-0) for more details.\n\n### Nested associations\n\nSometimes you need to load and authorize a parent resource when you have\na relationship between two resources and you are creating a new one or\nlisting all the children of that parent. Depending on your authorization\nmodel you migth authorize against the parent resource or against the child.\n\n```elixir\ndefmodule MyAppWeb.CommentController do\n\n  plug :load_and_authorize_resource,\n    model: Post,\n    id_name: \"post_id\",\n    only: [:new_comment, :create_comment]\n\n  # get /posts/:post_id/comments/new\n  def new_comment(conn, _params) do\n    # ...\n  end\n\n  # post /posts/:post_id/comments\n  def new_comment(conn, _params) do\n    # ...\n  end\nend\n```\n\nIt will authorize using `Canada.Can` with following arguments:\n1. subject is `conn.assigns.current_user`\n2. action is `:new_comment` or `:create_comment`\n3. resource is `%Post{}` with `conn.params[\"post_id\"]`\n\nThanks to the `:requried` set to true by default this plug will call `not_found_handler` if the `Post` with given `post_id` does not exists.\nIf for some reason you want to disable it, set `required: false` in opts.\n\n\u003e ### Deprecated {: .warning}\n\u003e\n\u003e The `:persisted` is deprecated as of 2.0.0-dev and will be removed in Canary 2.1.0\n\u003e Please follow the [Upgrade guide to 2.0.0](docs/upgrade.md#upgrading-from-canary-1-2-0-to-2-0-0) for more details.\n\n### Implementing Canada.Can for an anonymous user\n\nYou may wish to define permissions for when there is no logged in current user (when `conn.assigns.current_user` is `nil`).\nIn this case, you should implement `Canada.Can` for `nil` like so:\n\n```elixir\ndefimpl Canada.Can, for: Atom do\n  # When the user is not logged in, all they can do is read Posts\n  def can?(nil, :show, %Post{}), do: true\n  def can?(nil, _, _), do: false\nend\n```\n\n### Specifing database field\n\nYou can tell Canary to search for a resource using a field other than the default `:id` by using the `:id_field` option. Note that the specified field must be able to uniquely identify any resource in the specified table.\n\nFor example, if you want to access your posts using a string field called `slug`, you can use\n\n```elixir\nplug :load_and_authorize_resource, model: Post, id_name: \"slug\", id_field: \"slug\"\n```\n\nto load and authorize the resource `Post` with the slug specified by `conn.params[\"slug\"]` value.\n\nIf you are using Phoenix, your `web/router.ex` should contain something like:\n\n```elixir\nresources \"/posts\", PostController, param: \"slug\"\n```\n\nThen your URLs will look like:\n\n```\n/posts/my-new-post\n```\n\ninstead of\n\n```\n/posts/1\n```\n\n### Handling unauthorized actions\n\nBy default, when an action is unauthorized, Canary simply sets `conn.assigns.authorized` to `false`.\nHowever, you can configure a handler function to be called when authorization fails. Canary will pass the `Plug.Conn` to the given function. The handler should accept a `Plug.Conn` as its only argument, and should return a `Plug.Conn`.\n\nFor example, to have Canary call `Helpers.handle_unauthorized/1`:\n\n```elixir\nconfig :canary, unauthorized_handler: {Helpers, :handle_unauthorized}\n```\n\n### Handling resource not found\n\nBy default, when a resource is not found, Canary simply sets the resource in `conn.assigns` to `nil`. Like unauthorized action handling , you can configure a function to which Canary will pass the `conn` when a resource is not found:\n\n```elixir\nconfig :canary, not_found_handler: {Helpers, :handle_not_found}\n```\n\nYou can also specify handlers on an individual basis (which will override the corresponding configured handler, if any) by specifying the corresponding `opt` in the plug call:\n\n```elixir\nplug :load_and_authorize_resource Post,\n  unauthorized_handler: {Helpers, :handle_unauthorized},\n  not_found_handler: {Helpers, :handle_not_found}\n```\n\nTip: If you would like the request handling to stop after the handler function exits, e.g. when redirecting, be sure to call `Plug.Conn.halt/1` within your handler like so:\n\n```elixir\ndef handle_unauthorized(conn) do\n  conn\n  |\u003e put_flash(:error, \"You can't access that page!\")\n  |\u003e redirect(to: \"/\")\n  |\u003e halt\nend\n```\n\nNote: If both an `:unauthorized_handler` and a `:not_found_handler` are specified for `load_and_authorize_resource`, and the request meets the criteria for both, the `:unauthorized_handler` will be called first.\n\n## License\nMIT License. Copyright 2016 Chris Kelly.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcpjk%2Fcanary","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcpjk%2Fcanary","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcpjk%2Fcanary/lists"}