Full Code of cpjk/canary for AI

master 5efb8dca6012 cached
27 files
168.6 KB
44.8k tokens
112 symbols
1 requests
Download .txt
Repository: cpjk/canary
Branch: master
Commit: 5efb8dca6012
Files: 27
Total size: 168.6 KB

Directory structure:
gitextract_dt_ospw4/

├── .credo.exs
├── .formatter.exs
├── .github/
│   └── workflows/
│       └── elixir.yml
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config/
│   └── config.exs
├── docs/
│   ├── getting-started.md
│   └── upgrade.md
├── lib/
│   ├── canary/
│   │   ├── default_handler.ex
│   │   ├── error_handler.ex
│   │   ├── hooks.ex
│   │   ├── plugs.ex
│   │   └── utils.ex
│   └── canary.ex
├── mix.exs
└── test/
    ├── canary/
    │   ├── default_handler_test.exs
    │   ├── hooks_test.exs
    │   ├── plugs_test.exs
    │   └── utils_test.exs
    ├── support/
    │   ├── endpoint.ex
    │   ├── page_live.ex
    │   ├── post_live.ex
    │   └── router.ex
    └── test_helper.exs

================================================
FILE CONTENTS
================================================

================================================
FILE: .credo.exs
================================================
# This file contains the configuration for Credo and you are probably reading
# this after creating it with `mix credo.gen.config`.
#
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
%{
  #
  # You can have as many configs as you like in the `configs:` field.
  configs: [
    %{
      #
      # Run any config using `mix credo -C <name>`. If no config name is given
      # "default" is used.
      name: "default",
      #
      # These are the files included in the analysis:
      files: %{
        #
        # You can give explicit globs or simply directories.
        # In the latter case `**/*.{ex,exs}` will be used.
        included: ["lib/", "src/", "web/", "apps/"],
        excluded: [~r"/_build/", ~r"/deps/"]
      },
      #
      # If you create your own checks, you must specify the source files for
      # them here, so they can be loaded by Credo before running the analysis.
      requires: [],
      #
      # Credo automatically checks for updates, like e.g. Hex does.
      # You can disable this behaviour below:
      check_for_updates: true,
      #
      # If you want to enforce a style guide and need a more traditional linting
      # experience, you can change `strict` to `true` below:
      strict: false,
      #
      # If you want to use uncolored output by default, you can change `color`
      # to `false` below:
      color: true,
      #
      # You can customize the parameters of any check by adding a second element
      # to the tuple.
      #
      # To disable a check put `false` as second element:
      #
      #     {Credo.Check.Design.DuplicatedCode, false}
      #
      checks: [
        {Credo.Check.Consistency.ExceptionNames},
        {Credo.Check.Consistency.LineEndings},
        {Credo.Check.Consistency.MultiAliasImportRequireUse},
        {Credo.Check.Consistency.ParameterPatternMatching},
        {Credo.Check.Consistency.SpaceAroundOperators},
        {Credo.Check.Consistency.SpaceInParentheses},
        {Credo.Check.Consistency.TabsOrSpaces},

        # For some checks, like AliasUsage, you can only customize the priority
        # Priority values are: `low, normal, high, higher`
        {Credo.Check.Design.AliasUsage, priority: :low},

        # For others you can set parameters

        # If you don't want the `setup` and `test` macro calls in ExUnit tests
        # or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just
        # set the `excluded_macros` parameter to `[:schema, :setup, :test]`.
        {Credo.Check.Design.DuplicatedCode, excluded_macros: []},

        # You can also customize the exit_status of each check.
        # If you don't want TODO comments to cause `mix credo` to fail, just
        # set this value to 0 (zero).
        {Credo.Check.Design.TagTODO, exit_status: 2},
        {Credo.Check.Design.TagFIXME},

        {Credo.Check.Readability.FunctionNames},
        {Credo.Check.Readability.LargeNumbers},
        {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 80},
        {Credo.Check.Readability.ModuleAttributeNames},
        {Credo.Check.Readability.ModuleDoc},
        {Credo.Check.Readability.ModuleNames},
        {Credo.Check.Readability.NoParenthesesWhenZeroArity},
        {Credo.Check.Readability.ParenthesesInCondition},
        {Credo.Check.Readability.PredicateFunctionNames},
        {Credo.Check.Readability.PreferImplicitTry},
        {Credo.Check.Readability.RedundantBlankLines},
        {Credo.Check.Readability.Specs, false},
        {Credo.Check.Readability.StringSigils},
        {Credo.Check.Readability.TrailingBlankLine},
        {Credo.Check.Readability.TrailingWhiteSpace},
        {Credo.Check.Readability.VariableNames},
        {Credo.Check.Refactor.DoubleBooleanNegation},

        # {Credo.Check.Refactor.CaseTrivialMatches}, # deprecated in 0.4.0
        {Credo.Check.Refactor.ABCSize},
        {Credo.Check.Refactor.CondStatements},
        {Credo.Check.Refactor.CyclomaticComplexity},
        {Credo.Check.Refactor.FunctionArity},
        {Credo.Check.Refactor.MatchInCondition},
        {Credo.Check.Refactor.NegatedConditionsInUnless},
        {Credo.Check.Refactor.NegatedConditionsWithElse},
        {Credo.Check.Refactor.Nesting},
        {Credo.Check.Refactor.PipeChainStart, false},
        {Credo.Check.Refactor.UnlessWithElse},
        {Credo.Check.Refactor.VariableRebinding},

        {Credo.Check.Warning.BoolOperationOnSameValues},
        {Credo.Check.Warning.IExPry},
        {Credo.Check.Warning.IoInspect},
        {Credo.Check.Warning.NameRedeclarationByAssignment},
        {Credo.Check.Warning.NameRedeclarationByCase},
        {Credo.Check.Warning.NameRedeclarationByDef},
        {Credo.Check.Warning.NameRedeclarationByFn},
        {Credo.Check.Warning.OperationOnSameValues},
        {Credo.Check.Warning.OperationWithConstantResult},
        {Credo.Check.Warning.UnusedEnumOperation},
        {Credo.Check.Warning.UnusedFileOperation},
        {Credo.Check.Warning.UnusedKeywordOperation},
        {Credo.Check.Warning.UnusedListOperation},
        {Credo.Check.Warning.UnusedPathOperation},
        {Credo.Check.Warning.UnusedRegexOperation},
        {Credo.Check.Warning.UnusedStringOperation},
        {Credo.Check.Warning.UnusedTupleOperation},

        # Custom checks can be created using `mix credo.gen.check`.
        #
      ]
    }
  ]
}


================================================
FILE: .formatter.exs
================================================
[
  import_deps: [:plug, :phoenix, :phoenix_live_view],
  inputs: [
    "lib/**/*.ex",
    "config/*.exs",
    "test/**/*.exs",
    "mix.exs"
  ]
]


================================================
FILE: .github/workflows/elixir.yml
================================================
name: CI

on:
  push:
  pull_request:
    branches:
      - master

jobs:
  mix_test:
    name: mix test (OTP ${{matrix.otp}} | Elixir ${{matrix.elixir}})

    strategy:
      matrix:
        include:
          - elixir: "1.18"
            otp: "27.2"
          - elixir: "1.14"
            otp: "25.3"

    runs-on: ubuntu-20.04

    steps:
      - uses: actions/checkout@v4

      - name: Install Erlang and Elixir
        uses: erlef/setup-beam@v1
        with:
          otp-version: ${{ matrix.otp }}
          elixir-version: ${{ matrix.elixir }}

      - name: Restore deps and _build cache
        uses: actions/cache@v4
        with:
          path: |
            deps
            _build
          key: deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}
          restore-keys: |
            deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}

      - name: Install dependencies
        run: mix deps.get --only test

      - name: Remove compiled application files
        run: mix clean

      - name: Compile & lint dependencies
        run: mix compile --warnings-as-errors
        env:
          MIX_ENV: test

      - name: Run tests
        run: mix test


================================================
FILE: .gitignore
================================================
/_build
/deps
erl_crash.dump
*.ez
tags
/doc
/cover
*.beam
.history
.tool-version

================================================
FILE: .travis.yml
================================================
language: elixir
elixir:
  - 1.8
  - 1.7
  - 1.6
  - 1.5
  - 1.4
  
otp_release:
  - 21.1
  - 20.3
  - 19.3
  - 18.3

matrix:
  exclude:
    - elixir: 1.8
      otp_release: 19.3
    - elixir: 1.8
      otp_release: 18.3
    - elixir: 1.7
      otp_release: 18.3
    - elixir: 1.6
      otp_release: 18.3
    - elixir: 1.5
      otp_release: 21.1
    - elixir: 1.4
      otp_release: 21.1
    - elixir: 1.3
      otp_release: 21.1
    - elixir: 1.3
      otp_release: 20.3
script: mix test && mix credo


================================================
FILE: CHANGELOG.md
================================================
## Changelog

## v2.0.0-dev
  Canary 2.0.0 introduces authorization hooks for Phoenix LiveView. The Plug based authorization was refactored a bit to make the API cosistent. 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.

  * Enhancements
    * added support for authorization LiveView with `Canary.Hooks`
    * added `:error_handler` and ErrorHandler behaviour
    * added `:required` option, default to true

  * Dependency changes
    * Elixir ~> 1.14 is now required

  * Deprecations
    * The `:non_id_actions` option is deprecated and will be removed in Canary 2.1.0. Use separate `:authorize_resource` plug for `non_id_actions` and `:except` to exclude non_in_actions.
    * The `:persisted` option is deprecated and will be removed in Canary 2.1.0. Use `:required` instead.

## v1.2.0
  * Enhancements
    * Add `required` opt

## v1.1.0
  * Enhancements
    * Add `non_id_actions` opt

## v1.0
  * Bug fixes
    * Do not clobber resources in the `Conn` on index action if they are of the same model

## v0.14.2
  * Relax Ecto version requirements

## v0.14.1
  * Bug fixes
    * Use Macro.underscore/1 instead of Mix.Utils.underscore/1 to avoid :mix dependency on production

## v0.14.0
  * Enhancements
    * You can now 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.
  * Dependency changes
    * Elixir ~> 1.2 is now required
    * Ecto ~> 1.1 is now required

## v0.13.1

  * Enhancements
    * 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.
  * Bug Fixes
    * If more than one handler are specified and the first handler halts the request, the second handler will be skipped.

## v0.13.0

  * Enhancements
    * Canary can now be configured to call a user-defined function when a resource is not found. The function is specified and used in a similar manner to `:unauthorized_handler`.
  * Bug Fixes
    * Disabled protocol consolidation in order for tests to work on Elixir 1.2

## v0.12.2

  * Deprecations
    * Canary now looks for the current action in `conn.assigns.canary_action` rather than `conn.assigns.action` in order to avoid conflicts. The `action` key is deprecated.

## v0.12.0

* Enhancements
  * Canary can now be configured to call a user-defined function when authorization fails. Canary will pass the `Plug.Conn` for the request to the given function. The handler should accept a `Plug.Conn` as its only argument, and should return a `Plug.Conn`.
    * For example, to have Canary call `Helpers.handle_unauthorized/1`:
    ```elixir
    config :canary, unauthorized_handler: {Helpers, :handle_unauthorized}
    ```
    * You can also specify the `:unauthorized_handler` on an individual basis by specifying the `:unauthorized_handler`   `opt` in the plug call like so:
    ```elixir
    plug :load_and_authorize_resource Post, unauthorized_handler: {Helpers, :handle_unauthorized}
    ```

## v0.11.0

* Enhancements
  * Resources can now be loaded on `:new` and `:create` actions, when `persisted: true` is specified in the plug call. This allows parent resources to be loaded when a child is created. For example, if a `Post` resource has multiple `Comment` children, you may want to load the parent `Post` when creating a new `Comment`. You can load the parent `Post` with a separate
  ```elixir
  plug :load_and_authorize_resource, model: Post, id_name: "post_id", persisted: true, only: [:create]
  ```
  This will cause Canary to try to load the corresponding `Post` from the database when creating a `Comment` at the URL `/posts/:post_id/comments`

## v0.10.0

* Bug fix
  * Correctly checks `conn.assigns` for pre-existing resource

* Deprecations
  * Canary now favours looking for the current action in `conn.assigns.canary_action` rather than `conn.assigns.action` in order to avoid conflicts. The `action` key is deprecated

* Enhancements
  * The name of the id in `conn.params` can now be specified with the `id_name` opt


================================================
FILE: LICENSE
================================================
Copyright (c) 2016 Chris Kelly



Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:



The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.



THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
Canary
======
[![Actions Status](https://github.com/cpjk/canary/workflows/CI/badge.svg)](https://github.com/runhyve/canary/actions?query=workflow%3ACI)
[![Hex pm](https://img.shields.io/hexpm/v/canary.svg?style=flat)](https://hex.pm/packages/canary)

An 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.

Inspired by [CanCan](https://github.com/CanCanCommunity/cancancan) for Ruby on Rails.

[Read the docs](https://hexdocs.pm/canary/2.0.0-dev/getting-started.html)

# Canary 2.0.0

The `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).

## Installation

For the latest master (2.0.0-dev):

```elixir
defp deps do
  {:canary, github: "cpjk/canary"}
end
```

For the latest release:

```elixir
defp deps do
  {:canary, "~> 2.0.0-dev"}
end
```

Then run `mix deps.get` to fetch the dependencies.

## Quick start

Canary provides functions to be used as plugs or LiveView hooks to load and authorize resources:

`load_resource`, `authorize_resource`, `authorize_controller`*, and `load_and_authorize_resource`.

`load_resource` and `authorize_resource` can be used by themselves, while `load_and_authorize_resource` combines them both.

*Available only in plug based authentication*

In order to use Canary, you will need, at minimum:

- A [Canada.Can protocol](https://github.com/jarednorman/canada) implementation (a good place would be `lib/abilities.ex`)

- An Ecto record struct containing the user to authorize in `assigns.current_user` (the key can be customized - [see more](#overriding-the-default-user)).

- Your Ecto repo specified in your `config/config.exs`: `config :canary, repo: YourApp.Repo`

For 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.

For 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.


### load_resource

Loads 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.<resource_name>`, where `resource_name` is inferred from the model name.

<!-- tabs-open -->
### Conn Plugs example
```elixir
plug :load_resource, model: Project.Post
```

Will load the `Project.Post` having the id given in `conn.params["id"]` through `YourApp.Repo`, and assign it to `conn.assigns.post`.

### LiveView Hooks example
```elixir
mount_canary :load_resource, model: Project.Post
```

Will load the `Project.Post` having the id given in `params["id"]` through `YourApp.Repo`, and assign it to `socket.assigns.post`
<!-- tabs-close -->

### authorize_resource

Checks 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.

For Phoenix applications, Canary determines the action automatically.
For non-Phoenix applications, or to override the action provided by Phoenix, simply ensure that `assigns.canary_action` contains an atom specifying the action.

For the LiveView on `handle_params` it uses `socket.assigns.live_action` as action, on `handle_event` it uses the event name as action.



In 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).

### load_and_authorize_resource

Authorizes the resource and then loads it if authorization succeeds. Again, the resource is loaded into `assigns.<resource_name>`.

In the following example, the `Post` with the same `user_id` as the `current_user` is only loaded if authorization succeeds.

## Usage Example

Let's say you have a Phoenix application with a `Post` model, and you want to authorize the `current_user` for accessing `Post` resources.

Let's suppose that you have a file named `lib/abilities.ex` that contains your Canada authorization rules like so:

```elixir
defimpl Canada.Can, for: User do
  def can?(%User{ id: user_id }, action, %Post{ user_id: user_id })
    when action in [:show], do: true

  def can?(%User{ id: user_id }, _, _), do: false
end
```

### Example for Conn Plugs

In your `web/router.ex:` you have:

```elixir
get "/posts/:id", PostController, :show
delete "/posts/:id", PostController, :delete
```

To automatically load and authorize on the `Post` having the `id` given in the params, you would add the following plug to your `PostController`:

```elixir
plug :load_and_authorize_resource, model: Post
```

In this case, on `GET /posts/12` authorization succeeds, and the `Post` specified by `conn.params["id]` will be loaded into `conn.assigns.post`.

However, on `DELETE /posts/12`, authorization fails and the `Post` resource is not loaded.

### Example for LiveView Hooks

In your `web/router.ex:` you have:

```elixir
live "/posts/:id", PostLive, :show
```

and in your PostLive module `web/live/post_live.ex`:

```elixir
defmodule MyAppWeb.PostLive do
  use MyAppWeb, :live_view

  def render(assigns) do
    ~H"""
    Post id: {@post.id}
    <button phx-click="delete">Delete</button>
    """
  end

  def mount(_params, _session, socket), do: {:ok, socket}

  def handle_event("delete", _params, socket) do
    # Do the action
    {:noreply, update(socket, :temperature, &(&1 + 1))}
  end
end
```

To automatically load and authorize on the `Post` having the `id` given in the params, you would add the following hook to your `PostLive`:

```elixir
mount_hook :load_and_authorize_resource, model: Post
```

In 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`.

However, when the `delete` event will be triggered, authorization fails and the `Post` resource is not loaded. Socket will be halted.

### Excluding actions

To exclude an action from any of the plugs, pass the `:except` key, with a single action or list of actions.

For example,

Single action form:

```elixir
plug :load_and_authorize_resource, model: Post, except: :show

mount_canary :load_and_authorize_resource, model: Post, except: :show
```

List form:

```elixir
plug :load_and_authorize_resource, model: Post, except: [:show, :create]

mount_canary :load_and_authorize_resource, model: Post, except: [:show, :create]
```

### Authorizing only specific actions

To 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.

For example,

Single action form:

```elixir
plug :load_and_authorize_resource, model: Post, only: :show

mount_canary :load_and_authorize_resource, model: Post, only: :show
```

List form:

```elixir
plug :load_and_authorize_resource, model: Post, only: [:show, :create]

mount_canary :load_and_authorize_resource, model: Post, only: [:show, :create]
```

> Note: Having both `:only` and `:except` in opts is invalid. Canary will raise `ArgumentError` "You can't use both :except and :only options"

### Overriding the default user

Globally, the default key for finding the user to authorize can be set in your configuration as follows:

```elixir
config :canary, current_user: :some_current_user
```

In this case, canary will look for the current user record in `assigns.some_current_user`.

The current user key can also be overridden for individual plugs as follows:

```elixir
plug :load_and_authorize_resource, model: Post, current_user: :current_admin

mount_canary :load_and_authorize_resource, model: Post, current_user: :current_admin
```

### Specifying resource_name

To specify the name under which the loaded resource is stored, pass the `:as` flag in the plug declaration.

For example,

```elixir
plug :load_and_authorize_resource, model: Post, as: :new_post

mount_canary :load_and_authorize_resource, model: Post, as: :new_post
```

will load the post into `assigns.new_post`

### Preloading associations

Associations can be preloaded with `Repo.preload` by passing the `:preload` option with the name of the association:

```elixir
plug :load_and_authorize_resource, model: Post, preload: :comments

mount_canary :load_and_authorize_resource, model: Post, preload: :comments
```

### Non-id actions

To 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.

To authorize such actions use `authorize_resource` plug with `required: false` option

```elixir
plug :authorize_resource, model: Post, only: [:index, :new, :create], required: false

mount_canary :authorize_resource, model: Post, only: [:index, :new, :create], required: false
```

For example, when authorizing access to the `Post` resource, you should use

```elixir
def can?(%User{}, :index, Post), do: true
```

instead of

```elixir
def can?(%User{}, :index, %Post{}), do: true
```

> ### Deprecated {: .warning}
>
> The `:non_id_actions` is deprecated as of 2.0.0-dev and will be removed in Canary 2.1.0
> 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.

### Nested associations

Sometimes you need to load and authorize a parent resource when you have
a relationship between two resources and you are creating a new one or
listing all the children of that parent. Depending on your authorization
model you migth authorize against the parent resource or against the child.

```elixir
defmodule MyAppWeb.CommentController do

  plug :load_and_authorize_resource,
    model: Post,
    id_name: "post_id",
    only: [:new_comment, :create_comment]

  # get /posts/:post_id/comments/new
  def new_comment(conn, _params) do
    # ...
  end

  # post /posts/:post_id/comments
  def new_comment(conn, _params) do
    # ...
  end
end
```

It will authorize using `Canada.Can` with following arguments:
1. subject is `conn.assigns.current_user`
2. action is `:new_comment` or `:create_comment`
3. resource is `%Post{}` with `conn.params["post_id"]`

Thanks 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.
If for some reason you want to disable it, set `required: false` in opts.

> ### Deprecated {: .warning}
>
> The `:persisted` is deprecated as of 2.0.0-dev and will be removed in Canary 2.1.0
> 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.

### Implementing Canada.Can for an anonymous user

You may wish to define permissions for when there is no logged in current user (when `conn.assigns.current_user` is `nil`).
In this case, you should implement `Canada.Can` for `nil` like so:

```elixir
defimpl Canada.Can, for: Atom do
  # When the user is not logged in, all they can do is read Posts
  def can?(nil, :show, %Post{}), do: true
  def can?(nil, _, _), do: false
end
```

### Specifing database field

You 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.

For example, if you want to access your posts using a string field called `slug`, you can use

```elixir
plug :load_and_authorize_resource, model: Post, id_name: "slug", id_field: "slug"
```

to load and authorize the resource `Post` with the slug specified by `conn.params["slug"]` value.

If you are using Phoenix, your `web/router.ex` should contain something like:

```elixir
resources "/posts", PostController, param: "slug"
```

Then your URLs will look like:

```
/posts/my-new-post
```

instead of

```
/posts/1
```

### Handling unauthorized actions

By default, when an action is unauthorized, Canary simply sets `conn.assigns.authorized` to `false`.
However, 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`.

For example, to have Canary call `Helpers.handle_unauthorized/1`:

```elixir
config :canary, unauthorized_handler: {Helpers, :handle_unauthorized}
```

### Handling resource not found

By 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:

```elixir
config :canary, not_found_handler: {Helpers, :handle_not_found}
```

You 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:

```elixir
plug :load_and_authorize_resource Post,
  unauthorized_handler: {Helpers, :handle_unauthorized},
  not_found_handler: {Helpers, :handle_not_found}
```

Tip: 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:

```elixir
def handle_unauthorized(conn) do
  conn
  |> put_flash(:error, "You can't access that page!")
  |> redirect(to: "/")
  |> halt
end
```

Note: 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.

## License
MIT License. Copyright 2016 Chris Kelly.


================================================
FILE: config/config.exs
================================================
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
import Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for third-
# party users, it should be done in your mix.exs file.

# Sample configuration:
#
#     config :logger, :console,
#       level: :info,
#       format: "$date $time [$level] $metadata$message\n",
#       metadata: [:user_id]

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
#     import_config "#{Mix.env}.exs"


================================================
FILE: docs/getting-started.md
================================================
# Getting Started

This guide introduces **Canary**, an authorization library for **Elixir** applications using `Plug` and `Phoenix.LiveView`. It restricts resource access based on user permissions and automatically loads and assigns resources.

Canary provides three primary functions to be used as *plugs* or *LiveView hooks* to manage resources:

- `load_resource`
- `authorize_resource`
- `load_and_authorize_resource`

## Glossary

### Subject

The key name used to fetch the subject from `assigns`. This subject is passed to `Canada.Can` to evaluate permissions. By default, it is `:current_user`.

To configure this in your module:

```elixir
config :canary, current_user: :user
```

You can override this setting per plug/mounted hook by specifying `:current_user`.

### Action

For **Phoenix applications and Plug-based pages**, Canary determines the action automatically from `conn.private.phoenix_action`. In **non-Phoenix applications**, or when overriding Phoenix's default action behavior, set `conn.assigns.canary_action` with an atom specifying the action.

For **LiveView**:

- In `handle_params`, Canary uses `socket.assigns.live_action`.
- In `handle_event`, Canary uses the `event_name` (converted from a string to an atom for consistency).

Actions can be limited using `:only` or `:except` options; otherwise, they apply to all actions.

### Resource
For `load_resource` and `load_and_authorize_resource`, Canary checks if the resource is already assigned. If not, it fetches the resource from the repository using:

- `:id_name` from `params` (default: `"id"`).
- `:id_field` in the struct (default: `:id`).

By default, **a resource is required**. That means the resource must be present in `conn.assigns` or `socket.assigns`. It's fetched using the `:model` name, which can be overridden with the `:as` option.

If it cannot be found, an error is handled. To make it optional, set `:required` to `false`. In this case, the resource module name is used instead of a loaded struct.

You can also use `:preload` to preload associations. See `Ecto.Query.preload/3` for more details.

For `authorize_resource`, the resource must be present in `conn.assigns` or `socket.assigns`. By default, it fetches the resource using the `:model` name, which can be overridden with the `:as` option.

### Load Resource

Loads a resource from the database using the specified **Ecto repo** and model. It assigns the result to `assigns.<resource_name>`, where `resource_name` is inferred from the model.

### Authorize Resource

Checks if the **subject** can perform a given action on a resource. The result (`true`/`false`) is assigned to `assigns.authorized`. The developer decides how to handle this result.

### Load and Authorize Resource

A combination of **Load Resource** and **Authorize Resource** in a single function.

## Configuration

To use Canary, you need to configure it in `config/config.exs`. All settings, except for `:repo`, can be overridden when using the plug or hook.

### Available Configuration Options

| Name | Description | Example |
| --- | --- | --- |
| `:repo` | The Repo module used in your application. | `YourApp.Repo` |
| `:current_user` | The key name used to fetch the user from assigns. This value will be used as the `subject` for `Canada.Can` to evaluate permissions. Defaults to `:current_user`. | `:current_member` |
| `:error_handler` | A module that implements the `Canary.ErrorHandler` behavior. It is used to handle `:not_found` and `:unauthorized` errors. Defaults to `Canary.DefaultHandler`. | `YourApp.ErrorHandler` |

### Deprecated Options

| Name | Description | Example |
| --- | --- | --- |
| `:not_found_handler` | A `{mod, fun}` tuple for handling not found errors. | `{YourApp.ErrorHandler, :handle_not_found}` |
| `:unauthorized_handler` | A `{mod, fun}` tuple for handling unauthorized errors. | `{YourApp.ErrorHandler, :handle_unauthorized}` |

> #### Info {: .info}
>
> The `:error_handler` option should be used instead of separate handlers for `:not_found` and `:unauthorized` errors.
> Handlers can still be overridden using plug or `mount_canary` options.

### Example Configuration

```elixir
config :canary,
  repo: YourApp.Repo,
  current_user: :current_user,
  error_handler: YourApp.ErrorHandler
```

### Overriding configuration

#### Authorize different subject
Sometimes, you may need to perform authorization for a different subject. You can override `:current_user` by passing options to the plug or hook.

<!-- tabs-open -->
### Conn Plugs
```elixir
import Canary.Plugs

plug :load_and_authorize_resource,
  model: Team,
  current_user: :current_member
```

With this override, the authorization check will use `conn.assigns.current_member` as the subject.


### LiveView Hooks
```elixir
use Canary.Hooks

mount_canary :load_and_authorize_resource,
  on: :handle_event,
  current_user: :current_member,
  model: Team
```

With this override, the authorization check for the `:handle_event` stage hook will use `socket.assigns.current_member` as the subject.
<!-- tabs-close -->


### Different error handler

If you want to override the global Canary error handler, you can override one of the functions: `:not_found_handler` or `:unauthorized_handler`.

<!-- tabs-open -->
### Conn Plugs
```elixir
plug :load_and_authorize_resource,
  model: Team,
  current_user: :current_member,
  not_found_handler: {CustomErrorHandler, :custom_not_found_handler},
  unauthorized_handler: {CustomErrorHandler, :custom_unauthorized_handler}
```

### LiveView Hooks
```elixir
use Canary.Hooks

mount_canary :load_and_authorize_resource,
  model: Team,
  current_user: :current_member,
  only: [:special_action]
  unauthorized_handler: {CustomErrorHandler, :special_unauthorized_handler}
```
<!-- tabs-close -->

The error handler should implement the `Canary.ErrorHandler` behavior.
Refer to the default implementation in `Canary.DefaultHandler`.

## Canary options
Canary Plugs and Hooks use the same configuration options.

### Available Options

| Name | Description | Example |
| --- | --- | --- |
| `:model` | The model module name used in your app. **Required** | `Post` |
| `:only` | Specifies the actions for which the plug/hook is enabled. | `[:show, :edit, :update]` |
| `:except` | Specifies the actions for which the plug/hook is disabled. | `[:delete]` |
| `:current_user` | The key name used to fetch the user from assigns. This value will be used as the `subject` for `Canada.Can` to evaluate permissions. Defaults to `:current_user`. Applies only to `authorize_resource` or `load_and_authorize_resource`. | `:current_member` |
| `:on` | Specifies the LiveView lifecycle stages where the hook should be attached. Defaults to `:handle_params`. **Available only in Canary.Hooks** | `[:handle_params, :handle_event]` |
| `:as` | Specifies the key name under which the resource will be stored in assigns. | `:team_post` |
| `:id_name` | Specifies the name of the ID in params. *Defaults to `"id"`*. | `:post_id` |
| `:id_field` | Specifies the database field name used to search for the `id_name` value. *Defaults to `"id"`*. | `:post_id` |
| `:required` | Determines if the resource is required. If not found, it triggers a not found error. *Defaults to `true`*. | `false` |
| `:not_found_handler` | A `{mod, fun}` tuple that overrides the default error handler for not found errors. | `{YourApp.ErrorHandler, :custom_handle_not_found}` |
| `:unauthorized_handler` | A `{mod, fun}` tuple that overrides the default error handler for unauthorized errors. | `{YourApp.ErrorHandler, :custom_handle_unauthorized}` |

### Deprecated Options

| Name | Description | Example |
| --- | --- | --- |
| `:non_id_actions` | Additional actions for which Canary will authorize based on the model name. | `[:index, :new, :create]` |
| `:persisted` | Forces the resource to always be loaded from the database. Defaults to `false`. **Available only in Canary.Plugs** | `true` |

### Examples

```elixir
  plug :load_and_authorize_resource,
    current_user: :current_member,
    model: Machine,
    preload: [:plan, :networks, :distribution, :job, ipv4: [:ip_pool], hypervisor: :region]

  plug :load_resource,
    model: Hypervisor,
    id_name: "hypervisor_id",
    only: [:new, :create],
    preload: [:region, :hypervisor_type, machines: [:networks, :plan, :distribution]],

  plug :load_and_authorize_resource,
    model: Hypervisor,
    preload: [
      :region,
      :hypervisor_type,
      machines:
        {Hypervisors.preload_active_machines, [:plan, :distribution, :hypervisor, :networks]}
    ]

  mount_canary :authorize_resource,
    on: [:handle_params, :handle_event],
    current_user: :current_member,
    model: Machine,
    only: [:index, :new],
    required: false

  mount_canary :load_and_authorize_resource,
    on: [:handle_event],
    current_user: :current_member,
    model: Machine,
    only: [:start, :stop, :restart, :poweroff]
```

## Plug and Hooks


`Canary.Plugs` and `Canary.Hooks` should work the same way in most cases, providing a unified approach to authorization for both Plug-based controllers and LiveView.

- **Shared Functionality:**
  Both Plugs and Hooks allow for resource loading and authorization using similar configuration options. This ensures consistency across different parts of your application.

- **Differences:**
  - `Canary.Plugs` is designed for use in traditional Phoenix controllers and pipelines.
  - `Canary.Hooks` is specifically built for LiveView and integrates with lifecycle events such as `:handle_params` and `:handle_event`.

- **Configuration Compatibility:**
  Most options, such as `:model`, `:current_user`, `:only`, `:except`, and error handlers, function identically in both Plugs and Hooks. However, `Canary.Hooks` includes the `:on` option, allowing you to specify which LiveView lifecycle stage the authorization should run on.

By keeping their behavior aligned, Canary ensures a seamless developer experience, whether you're working with traditional controller-based actions or real-time LiveView interactions.
### Authorize Resource

The `authorize_resource` function checks whether the subject, typically stored in `assigns` under `:current_user`, is authorized to access a given resource. If the `:current_user` is not authorized, it sets `assigns.authorized` to `false` and calls the `handle_unauthorized/1` function from the `:error_handler` module configured in `config.exs` or the `:unauthorized_handler` specified in the options.

#### Authorization Logic

The authorization check is performed using the `can?/3` function from the `Canada.Can` protocol implemeted for `subject`:

```elixir
can?(subject, action, resource)
```

where:

1. **Subject** – The entity being authorized, typically fetched from `assigns.current_user`.
   - By default, Canary looks for `:current_user`.
   - This key can be overridden via the `opts` or globally in `Application.get_env(:canary, :current_user, :current_user)`.

2. **Action** – The current action being performed.

3. **Resource** – The resource being accessed.
   - If the resource is already loaded, it is taken from `assigns`.
   - If the resource is not loaded and not required, the model name is used instead.

#### Example Usage

```elixir
# Replace `plug` with `mount_canary` for LiveView Hooks
plug :authorize_resource,
  current_user: :current_member,
  model: Event,
  as: :public_event
```

In this example:

1. The `authorize_resource` function checks whether `:current_member` (instead of the default `:current_user`) is authorized to access the `Event` resource.
2. The resource is expected to be available in `assigns.public_event`.
3. If the user is unauthorized, `assigns.authorized` is set to false, and the `unauthorized_handler` is triggered.

### Load Resource

The `load_resource` function fetches a resource based on an ID provided in `params` and assigns it to `assigns`. By default, it uses the `"id"` key from `params` and retrieves the resource from the database using the `:id` field of the model specified in `opts[:model]`. The loaded resource is stored under `assigns` using a key derived from the model module name.

#### Customizing the Load Behavior

You can modify the default behavior with the following options:

- **`:id_name`** – Override the default `"id"` param key.
- **`:id_field`** – Change the field used to query the resource in the database.
- **`:as`** – Override the default `assigns` key where the resource is stored.
- **`:required`** - When set to `false` it will assign `nil` instad calling the `not_found_handler`.

#### Example Usage

```elixir
# Replace `plug` with `mount_canary` for LiveView Hooks
plug :load_resource,
  model: Event,
  as: :public_event,
  id_name: "uuid",
  id_field: :uuid,
  required: false
```

In this example:

1. `load_resource` fetches the `"uuid"` from `params`.
2. It queries `Event` using the `:uuid` field in the database.
3. The result is assigned to `assigns.public_event`.
4. If no matching `Event` is found, `assigns.public_event` will be set to `nil`.

To trigger the `not_found_handler` when the resource is missing, ensure the `:required` flag is **not explicitly set to** `false` (it defaults to `true`).


### Load and Authorize Resource

The `load_and_authorize_resource` function combines two operations:

1. **Loading the Resource** – Fetches the resource based on an ID from `params` and assigns it to `assigns`, similar to `load_resource`.
2. **Authorizing the Resource** – Checks whether the subject (by default, `:current_user`) is authorized to access the resource, using `authorize_resource`.

This function ensures that resources are both retrieved and access-controlled within a single step.

> #### Error handler order {: .info}
>
> If both `:unauthorized_handler` and `: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.


## Non-ID Actions

For actions that do not require loading a specific resource (such as `:index`, `:new`, and `:create`), use `:authorize_resource` instead of `:load_resource` or `:load_and_authorize_resource`.
Ensure that these functions are limited to actions where resource loading is necessary.

By default, the `:required` option is set to `true`, meaning that if the resource cannot be found in the repository, the `not_found_handler` will be called.
Setting `:required` to `false` allows the resource to be assigned as `nil`, in which case the model module name will be used as the resource when calling `can?/3`.

### Example Usage

```elixir
plug :authorize_resource,
  model: Post,
  only: [:index, :new, :create],
  required: false

plug :load_and_authorize_resource,
  model: Post,
  except: [:index, :create, :new]
```

### Loading All Resources in `:index` Action
If you need to load multiple resources for the `:index` action, you can either use a plug or load the resources directly within the `index/2` controller action.

#### Option 1: Using a Plug
```elixir
plug :load_all_resources when action in [:index]

defp load_all_resources(conn, _opts) do
  assign(conn, :posts, Posts.list_posts())
end
```

#### Option 2: Loading Directly in the Controller Action
```elixir
def index(conn, _params) do
  posts = Posts.list_posts()
  render(conn, "index.html", posts: posts)
end
```

## Nested Resources

Sometimes, you need to load and authorize a parent resource when dealing with nested relationships—such as when creating a child resource or listing all children of a parent. With the default `:required` set to true, if the parent resource is not found, the `not_found_handler` will be called.

### Example Usage

When loading and authorizing a `Post` resource that `has_many` `Comment` resources:

```elixir
# Load and authorize the parent (Post)
plug :load_and_authorize_resource,
  model: Post,
  id_name: "post_id",
  only: [:create_comment]

# Authorize action the child (Comment)
plug :authorize_resource,
  model: Comment,
  only: [:create_comment, :save_comment],
  required: false
```

#### Explanation

1. The first plug loads and authorizes the parent `Post` resource using the `post_id` from `params` in the URL (`/posts/:post_id/comments`).
   - The `:required` option ensures that if the Post is missing, the `not_found_handler` is called.
2. The second plug authorizes actions on the child `Comment` resource.
  - Since this is a **non-ID action**, `authorize_resource` is used.
  - The `Comment` module name is passed as the resource to `can?/3` since no specific `Comment` does not exists yet.

This approach ensures that authorization is enforced correctly in nested resource scenarios.

## Defining Permissions

To perform authorization checks, you need to implement the [`Canada.Can` protocol](https://github.com/jarednorman/canada) for each subject that requires permission validation.
By default, Canary uses `:current_user` from Plug or LiveView assigns as the subject.

### Example: Defining Permissions for an Authenticated User

Assume your application has a `User` module for authentication.
You can define permissions in `lib/abilities/user.ex`:

```elixir
defimpl Canada.Can, for: User do
  # Super admin can do everything
  def can?(%User{role: "superadmin"}, _action, _resource), do: true

  # Post owner can view and modify their own posts
  def can?(%User{id: user_id}, action, %Post{user_id: user_id})
    when action in [:show, :edit, :update], do: true

  # Deny all other actions by default
  def can?(%User{id: user_id}, _, _), do: false
end
```

### Handling Anonymous Users

If the subject (`:current_user` in assigns) is `nil`, and the authorization check is performed then `can/3` will be performed against `Atom`.

For anonymous users, define permissions, for example: `lib/abilities/anonymous.ex`:
```elixir
defimpl Canada.Can, for: Atom do
  # Allow anonymous users to register
  def can?(nil, :new, User), do: true
  def can?(nil, :create, User), do: true
  def can?(nil, :confirm, User), do: true

  # Allow anonymous users to create sessions
  def can?(nil, :new, Session), do: true
  def can?(nil, :create, Session), do: true

  # Deny all other actions
  def can?(_, _action, _model), do: false
end
```

Defining permissions for `Atom` and `nil` subjects is optional.
If your application enforces authentication using a plug like `:require_authenticated_user` in the router pipeline, this may not be necessary.


## Error handling

### Handling Unauthorized Actions

By default, when subject is unauthorized to access an action, Canary sets `assigns.authorized` to `false`.
However, you can configure a custom handler function to be called when authorization fails.
Canary will pass the `Plug.Conn` or `Phoenix.LiveView.Socket` to the specified function, which should accept `conn` or `socket` as its only argument and return a `Plug.Conn` or tuple `{:halt, socket}`.

The error handler should implement the `Canary.ErrorHandler` behavior.
Refer to the default implementation in `Canary.DefaultHandler`.

For example, to have Canary call `ErrorHandler.handle_unauthorized/1`:

```elixir
config :canary, error_handler: ErrorHandler
```

> #### LiveView Hook handlers
>
> In LiveView, the error handler should return `{:halt, socket}`.
> For `handle_params`, it should also perform a redirect.


### Handling Resource Not Found

By default, when a resource is not found, Canary sets the resource in `assigns` to `nil`.
Similar to unauthorized action handling, you can configure a function that Canary will call when a resource is missing. This function will receive the `conn` (for Plugs) or `socket` (for LiveView).

```elixir
config :canary, error_handler: ErrorHandler
```

### Overriding Handlers Per Action

You can specify custom handlers per action using `opts` in the `plug` or `mount_canary` call.
These handlers will override any globally configured error handlers.

<!-- tabs-open -->

### Conn Plugs

```elixir
plug :load_and_authorize_resource Post,
  unauthorized_handler: {Helpers, :handle_unauthorized},
  not_found_handler: {Helpers, :handle_not_found}
```

> **Tip:** If you want to stop request handling after the handler function executes (e.g., for a redirect),
> be sure to call `Plug.Conn.halt/1` within your handler:

```elixir
def handle_unauthorized(conn) do
  conn
  |> put_flash(:error, "You can't access that page!")
  |> redirect(to: "/")
  |> halt()
end
```

### LiveView Hooks

```elixir
mount_canary :load_and_authorize_resource Post,
  unauthorized_handler: {Helpers, :handle_unauthorized},
  not_found_handler: {Helpers, :handle_not_found}
```

> **Tip:** If you want to stop request handling after the handler function executes (e.g., for a redirect),
> be sure to call `Plug.Conn.halt/1` within your handler:

```elixir
def handle_unauthorized(socket) do
  {:halt, Phoenix.LiveView.redirect(socket, to: "/")}
end
```
<!-- tabs-close -->

> #### Error handler order {: .info}
>
> 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.

================================================
FILE: docs/upgrade.md
================================================
# Upgrade guides

## Upgrading from Canary 1.2.0 to 2.0.0

### Update Your Non-ID Actions

> Since 2.0.0, the `:persisted` and `:non_id_actions` options have been deprecated and will be removed in Canary 2.1.0.

You need to update plug calls. Using `:authorize_resource` for actions where there is no actual load action is more explicit.

Let's assume you currently have the following plug call:
```elixir
  plug :load_and_authorize_resource,
    model: Network,
    non_id_actions: [:index, :create, :new],
    preload: [:hypervisor]
```

Now let's break it apart:
```elixir
  plug :authorize_resource,
    model: Network,
    only: [:index, :create, :new],
    required: false

  plug :load_and_authorize_resource,
    model: Network,
    except: [:index, :create, :new],
    preload: [:hypervisor]
```

For non-ID actions, there is a separate plug for authorization. The `required: false` option marks the resource as optional during authorization checks, and the model module name is used. Essentially, this is how :non_id_actions worked.
For actions other than `:index`, `:create`, and `:new`, it will load and authorize resources as usual.
To load all resources in the `:index` action, you can set up a plug or add the load function directly in `index/2`.

```elixir
  # using a plug

  plug :load_all_resources when action in [:index]
  defp load_all_resources(conn, _opts) do
    assign(:networks, Hypervisors.list_hypervisor_networks(hypervisor))
  end

  # directly in the controller action

  def index(conn, _params) do
    networks = Hypervisors.list_hypervisor_networks(hypervisor)
    render(conn, "index.html", networks: networks)
  end
```

### Remove `:persisted` option

With the [update non-id action](#update-your-non-id-actions) the `:persisted` is no longer required.

================================================
FILE: lib/canary/default_handler.ex
================================================
defmodule Canary.DefaultHandler do
  @moduledoc """
  The fallback Canary handler.

  This module is used primarily as a backwards compatibility for the `:not_found_handler` and `:unauthorized_handler`.
  It uses old configuration values to determine how to handle the error.

  If you are using `Canary` only with `Plug` based authorization then you can still
  use the `:not_found_handler` and `:unauthorized_handler` configuration values.
  Otherwise, you should implement the `Canary.ErrorHandler` behaviour in your own module.
  """
  @moduledoc since: "2.0.0"

  @behaviour Canary.ErrorHandler

  @doc """
  The default handler for when a resource is not found.
  For Plug based authorization it will use the global `:not_found_handler` or return the conn.

  For LiveView base authorization it will halt socket.
  """
  @impl true
  def not_found_handler(%Plug.Conn{} = conn) do
    case Application.get_env(:canary, :not_found_handler) do
      {mod, fun} -> apply(mod, fun, [conn])
      _ -> conn
    end
  end
  def not_found_handler(%Phoenix.LiveView.Socket{} = socket) do
    {:halt, Phoenix.LiveView.redirect(socket, to: "/")}
  end

  @doc """
  The default handler for when a resource is not authorized.
  For Plug based authorization it will use the global `:unauthorized_handler` or return the conn.

  For LiveView base authorization it will halt socket.
  """
  @impl true
  def unauthorized_handler(%Plug.Conn{} = conn) do
    case Application.get_env(:canary, :unauthorized_handler) do
      {mod, fun} -> apply(mod, fun, [conn])
      _ -> conn
    end
  end
  def unauthorized_handler(%Phoenix.LiveView.Socket{} = socket) do
    {:halt, Phoenix.LiveView.redirect(socket, to: "/")}
  end
end


================================================
FILE: lib/canary/error_handler.ex
================================================
defmodule Canary.ErrorHandler do
  @moduledoc """
  Specifies the behavior for handling errors in Canary.


  """
  @moduledoc since: "2.0.0"

  @doc """
  Handles the case where a resource is not found.
  """
  @callback not_found_handler(Plug.Conn.t) :: Plug.Conn.t
  @callback not_found_handler(Phoenix.LiveView.Socket.t) :: {:halt, Phoenix.LiveView.Socket.t}

  @doc """
  Handles the case where a resource is not authorized.
  """
  @callback unauthorized_handler(Plug.Conn.t) :: Plug.Conn.t
  @callback unauthorized_handler(Phoenix.LiveView.Socket.t) :: {:halt, Phoenix.LiveView.Socket.t}
end


================================================
FILE: lib/canary/hooks.ex
================================================
if Code.ensure_loaded?(Phoenix.LiveView) do
  defmodule Canary.Hooks do
    @moduledoc """

    Hooks functions for loading and authorizing resources for the LiveView events.
    If you want to authorize `handle_params` and `handle_event` LiveView callbacks
    you can use `mount_canary` macro to attach the hooks.

    You can think about the `mount_canary` as something similar to `plug` but for LiveView events.

    For `handle_params` it uses `socket.assigns.live_action` as `:action`.
    For `handle_event` it uses the event name as `:action`.

    > Note that the `event_name` is a string - but in Canary it's converted to an atom for consistency.

    `Canary.Hooks` and `Canary.Plugs` are separate modules but they share the same API.
    You can define plugs for standard pages and hooks for LiveView events with the same opts.

    For the authorization actions, when the `:required` is false (by default it's true) it might be nil.
    Then the `Canada.Can` implementation should be the module name of the model rather than a struct.

    ## Example
      ```elixir
      use Canary.Hooks

      mount_canary :load_and_authorize_resource,
        on: [:handle_params, :handle_event],
        model: Post,
        only: [:show, :edit, :update]

      mount_canary :authorize_resource,
        on: [:handle_event],
        model: Post,
        only: [:my_event],
        required: false

      # ...

      def handle_params(params, _uri, socket) do
        # resource is already loaded and authorized
        post = socket.assigns.post
      end

      def handle_event("my_event", _unsigned_params, socket) do
        # Only admin is allowed to perform my_event
      end

      ```

      `lib/abilities/user.ex`:

      ```elixir
      defimpl Canada.Can, for: User do

        def can?(%User{} = user, :my_event, Post), do: user.role == "admin"

        def can?(%User{id: id}, _, %Post{user_id: user_id}), do: id == user_id
      end
      ```
    """
    # Copyright 2025 Piotr Baj
    @moduledoc since: "2.0.0"

    import Canary.Utils
    import Canada.Can, only: [can?: 3]
    import Phoenix.LiveView, only: [attach_hook: 4]
    import Phoenix.Component, only: [assign: 3]

    alias Phoenix.LiveView.Socket

    @doc false
    defmacro __using__(_opts) do
      quote do
        import Canary.Hooks

        Module.register_attribute(__MODULE__, :canary_hooks, accumulate: true)

        # Register the Canary.Hooks.__before_compile__/1 callback to be called before Phoenix.LiveView.
        hooks = Module.delete_attribute(__MODULE__, :before_compile)
        @before_compile Canary.Hooks
        Enum.each(hooks, fn {mod, fun} ->
          @before_compile {mod, fun}
        end)
      end
    end

    @doc false
    defmacro __before_compile__(env) do
      stages =
        Module.get_attribute(env.module, :canary_hooks, [])
        |> Enum.reverse()

      wrapped_hooks = Enum.with_index(stages, &wrap_hooks/2)
      mount_attach = attach_hooks_on_mount(env, stages)

      [wrapped_hooks, mount_attach]
    end

    defp wrap_hooks({stage, hook, opts}, id) do
      name = hook_name(stage, hook, id)

      quote do
        def unquote(name)(hook_arg_1, hook_arg_2, %Socket{} = socket) do
          metadata = %{hook: unquote(hook), stage: unquote(stage), opts: unquote(opts)}
          handle_hook(metadata, [hook_arg_1, hook_arg_2, socket])
        end
      end
    end

    defp attach_hooks_on_mount(env, stages) do
      hooks =
        Enum.with_index(stages)
        |> Enum.map(fn {{stage, hook, _opts}, id} ->
          name = hook_name(stage, hook, id)
          {name, stage}
        end)

      quote bind_quoted: [module: env.module, hooks: hooks] do
        on_mount {Canary.Hooks, {:initialize, module, hooks}}
      end
    end

    defp hook_name(stage, hook, id) do
      String.to_atom("#{stage}_#{hook}_#{id}")
    end

    @doc false
    def handle_hook(metadata, [hook_arg_1, hook_arg_2, socket]) do
      %{hook: hook, stage: stage, opts: opts} = metadata

      case hook do
        :load_resource ->
          load_resource(stage, hook_arg_1, hook_arg_2, socket, opts)

        :authorize_resource ->
          authorize_resource(stage, hook_arg_1, hook_arg_2, socket, opts)

        :load_and_authorize_resource ->
          load_and_authorize_resource(stage, hook_arg_1, hook_arg_2, socket, opts)

        _ ->
          IO.warn(
            "Invalid type #{inspect(hook)} for Canary hook call. Please review defined hooks with mount_canary/2.",
            module: __MODULE__
          )

          {:cont, socket}
      end
    end

    @doc """
    Mount canary authorization hooks on the current module.
    It creates a wrapper function to handle_params and handle_event,
    and attaches the hooks to the Live View.

    ## Example
      ```
      mount_canary :load_and_authorize_resource,
        model: Post,
        only: [:edit: :update]
      ```
    """
    defmacro mount_canary(type, opts) do
      stages = get_stages(opts)

      if Enum.empty?(stages),
        do:
          IO.warn("mount_canary called with empty :on stages",
            module: __CALLER__.module,
            file: __CALLER__.file,
            line: __CALLER__.line
          )

      Enum.reduce(stages, [], fn stage, acc ->
        [put_canary_hook(__CALLER__.module, stage, type, opts) | acc]
      end)
      |> Enum.reverse()
    end

    defp put_canary_hook(module, stage, type, opts) do
      quote do
        Module.put_attribute(
          unquote(module),
          :canary_hooks,
          {unquote(stage), unquote(type), unquote(opts)}
        )
      end
    end

    @doc false
    def on_mount({:initialize, mod, stages}, _params, _session, %Socket{} = socket) do
      socket =
        Enum.reduce(stages, socket, fn {name, stage}, socket ->
          fun = Function.capture(mod, name, 3)
          attach_hook(socket, name, stage, fun)
        end)

      {:cont, socket}
    end

    def on_mount(_, :not_mounted_at_router, _session, socket), do: {:cont, socket}

    @doc """
    Authorize the `:current_user` for the ginve resource. If the `:current_user` is not authorized it will halt the socket.

    For the authorization check, it uses the `can?/3` function from the `Canada.Can` module -

    `can?(subject, action, resource)` where:

    1. The subject is the `:current_user` from the socket assigns. The `:current_user` key can be changed in the `opts` or in the `Application.get_env(:canary, :current_user, :current_user)`. By default it's `:current_user`.
    2. The action for `handle_params` is `socket.assigns.live_action`, for `handle_event` it uses the event name.
    3. The resource is the loaded resource from the socket assigns or the model name if the resource is not loaded and not required.

    Required opts:

    * `:model` - Specifies the module name of the model to load resources from
    * `:on` - Specifies the LiveView lifecycle stages to attach the hook. Default :handle_params

    Optional opts:

    * `:only` - Specifies which actions to authorize
    * `:except` - Specifies which actions for which to skip authorization
    >  For `handle_params` it uses `socket.assigns.live_action` as `:action`.
    >  For `handle_event` it uses the event name as `:action`.

    * `:as` - Specifies the `resource_name` to get from assigns
    * `:current_user` - Specifies the key in the socket assigns to get the current user
    * `:required` - Specifies if the resource is required, when it's not assigned in socket it will halt the socket
    * `:unauthorized_handler` - Specify a handler function to be called if the action is unauthorized

    Example:

    ```elixir

    mount_canary :authorize_resource,
      model: Post,
      only: [:show, :edit, :update]
      current_user: :current_user

    mount_canary :authorize_resource,
      model: Post,
      as: :custom_resource_name,
      except: [:new, :create],
      unauthorized_handler: {ErrorHandler, :unauthorized_handler}

    ```
    """
    def authorize_resource(:handle_params, _params, _uri, %Socket{} = socket, opts) do
      action = socket.assigns.live_action
      do_authorize_resource(action, socket, opts)
    end

    def authorize_resource(:handle_event, event_name, _unsigned_params, %Socket{} = socket, opts) do
      action = String.to_atom(event_name)
      do_authorize_resource(action, socket, opts)
    end

    @doc """
    Loads the resource and assigns it to the socket. When resource is required it will
    halt the socket if the resource is not found.

    `load_resource` wrapper for attached hook functions, similar to the `load_resource/2` plug
    but for LiveView events on `:handle_params` and `:handle_event` stages.

    Required opts:

    * `:model` - Specifies the module name of the model to load resources from
    * `:on` - Specifies the LiveView lifecycle stages to attach the hook. Default :handle_params

    Optional opts:
    * `:only` - Specifies which actions to authorize
    * `:except` - Specifies which actions for which to skip authorization
    >  For `handle_params` it uses `socket.assigns.live_action` as `:action`.
    >  For `handle_event` it uses the event name as `:action`.

    * `:as` - Specifies the `resource_name` to use in assigns
    * `:preload` - Specifies association(s) to preload
    * `:id_name` - Specifies the name of the id in `params`, defaults to "id"
    * `:id_field` - Specifies the name of the ID field in the database for searching :id_name value, defaults to "id".
    * `:required` - Specifies if the resource is required, when it's not found it will halt the socket
    * `:not_found_handler` - Specify a handler function to be called if the resource is not found

    Example:

    ```elixir

    mount_canary :load_resource,
      model: Post,
      only: [:show, :edit, :update],
      preload: [:comments]

    mount_canary :load_resource,
      on: [:handle_params, :handle_event]
      model: Post,
      as: :custom_name,
      except: [:new, :create],
      preload: [:comments],
      not_found_handler: {ErrorHandler, :not_found_handler}

    ```
    """
    def load_resource(:handle_params, params, _uri, %Socket{} = socket, opts) do
      action = socket.assigns.live_action
      do_load_resource(action, socket, params, opts)
    end

    def load_resource(:handle_event, event_name, unsigned_params, %Socket{} = socket, opts) do
      action = String.to_atom(event_name)
      do_load_resource(action, socket, unsigned_params, opts)
    end

    @doc """
    Loads and autorize resource and assigns it to the socket. When resource is required it will
    halt the socket if the resource is not found. If the user is not authorized it will halt the socket.

    It combines `load_resource` and `authorize_resource` functions.

    Required opts:

    * `:model` - Specifies the module name of the model to load resources from
    * `:on` - Specifies the LiveView lifecycle stages to attach the hook. Default :handle_params

    Optional opts:
    * `:only` - Specifies which actions to authorize
    * `:except` - Specifies which actions for which to skip authorization
    >  For `handle_params` it uses `socket.assigns.live_action` as `:action`.
    >  For `handle_event` it uses the event name as `:action`.

    * `:as` - Specifies the `resource_name` to use in assigns
    * `:current_user` - Specifies the key in the socket assigns to get the current user
    * `:preload` - Specifies association(s) to preload
    * `:id_name` - Specifies the name of the id in `params`, defaults to "id"
    * `:id_field` - Specifies the name of the ID field in the database for searching :id_name value, defaults to "id".
    * `:required` - Specifies if the resource is required, when it's not found it will halt the socket, default true
    * `:not_found_handler` - Specify a handler function to be called if the resource is not found
    * `:unauthorized_handler` - Specify a handler function to be called if the action is unauthorized

    Example:

    ```elixir

    mount_canary :load_and_authorize_resource,
      model: Comments,
      id_name: :post_id,
      id_field: :post_id,
      only: [:comments]

    mount_canary :load_and_authorize_resource,
      model: Post,
      as: :custom_name,
      except: [:new, :create],
      preload: [:comments],
      error_handler: CustomErrorHandler
    ```

    """
    def load_and_authorize_resource(:handle_params, params, _uri, %Socket{} = socket, opts) do
      action = socket.assigns.live_action
      do_load_and_authorize_resource(action, params, socket, opts)
    end

    def load_and_authorize_resource(
          :handle_event,
          event_name,
          unsigned_params,
          %Socket{} = socket,
          opts
        ) do
      action = String.to_atom(event_name)
      do_load_and_authorize_resource(action, unsigned_params, socket, opts)
    end

    defp do_load_resource(action, socket, params, opts) do
      if action_valid?(action, opts) do
        assign(socket, :canary_action, action)
        |> load_resource(params, opts)
        |> verify_resource(opts)
      else
        {:cont, socket}
      end
    end

    defp do_load_and_authorize_resource(action, params, socket, opts) do
      if action_valid?(action, opts) do
        assign(socket, :canary_action, action)
        |> load_resource(params, opts)
        |> check_authorization(action, opts)
        |> verify_authorized_resource(opts)
      else
        {:cont, socket}
      end
    end

    defp do_authorize_resource(action, socket, opts) do
      if action_valid?(action, opts) do
        assign(socket, :canary_action, action)
        |> check_authorization(action, opts)
        |> verify_authorized_resource(opts)
      else
        {:cont, socket}
      end
    end

    # Check if the resource is already loaded in the socket assigns
    # If not we need to load and assign it
    defp load_resource(%Socket{} = socket, params, opts) do
      resource =
        case fetch_resource(socket, opts) do
          {:ok, resource} ->
            resource

          _ ->
            repo_get_resource(params, opts)
        end

      action = socket.assigns.canary_action
      assign(socket, get_resource_name(action, opts), resource)
    end

    # Fetch the resource from the socket assigns or nil
    defp fetch_resource(%Socket{} = socket, opts) do
      action = socket.assigns.canary_action
      case Map.get(socket.assigns, get_resource_name(action, opts), nil) do
        resource when is_struct(resource) ->
          if resource.__struct__ == opts[:model] do
            {:ok, resource}
          else
            nil
          end

        _ ->
          nil
      end
    end

    # Load the resource from the repo
    defp repo_get_resource(params, opts) do
      repo = Application.get_env(:canary, :repo)
      field_name = Keyword.get(opts, :id_field, "id")
      get_map_args = %{String.to_atom(field_name) => get_resource_id(params, opts)}

      repo.get_by(opts[:model], get_map_args)
      |> preload_if_needed(repo, opts)
    end

    # Perform the authorization check
    defp check_authorization(%Socket{} = socket, action, opts) do
      current_user_name =
        opts[:current_user] || Application.get_env(:canary, :current_user, :current_user)

      current_user = Map.fetch(socket.assigns, current_user_name)
      resource = fetch_resoruce_or_model(socket, opts)

      case {current_user, resource} do
        {{:ok, _current_user}, nil} ->
          assign(socket, :authorized, false)
        {{:ok, current_user}, _} ->
          assign(socket, :authorized, can?(current_user, action, resource))
        _ ->
          assign(socket, :authorized, false)
      end
    end

    # Fetch resource form assigns or model name if empty and not required
    defp fetch_resoruce_or_model(%Socket{} = socket, opts) do
      case fetch_resource(socket, opts) do
        {:ok, resource} ->
          resource

        _ ->
          if required?(opts) do
            nil
          else
            opts[:model]
          end
      end
    end

    # Verify if subject is authorized to perform action on resource
    defp verify_authorized_resource(%Socket{} = socket, opts) do
      authorized = Map.get(socket.assigns, :authorized, false)

      if authorized do
        verify_resource(socket, opts)
      else
        apply_error_handler(socket, :unauthorized_handler, opts)
      end
    end

    # Verify if the resource is loaded and if it is required
    defp verify_resource(%Socket{} = socket, opts) do
      is_required = required?(opts)
      resource = fetch_resource(socket, opts)

      if is_nil(resource) && is_required do
        apply_error_handler(socket, :not_found_handler, opts)
      else
        {:cont, socket}
      end
    end

     defp get_stages(opts) do
      Keyword.get(opts, :on, :handle_params)
      |> validate_stages()
    end

    defp validate_stages(stage) when is_atom(stage), do: validate_stages([stage])

    defp validate_stages(stages) when is_list(stages) do
      allowed_satges = [:handle_params, :handle_event]
      Enum.filter(stages, &Enum.member?(allowed_satges, &1))
    end
  end
end


================================================
FILE: lib/canary/plugs.ex
================================================
defmodule Canary.Plugs do
  import Canary.Utils
  import Canada.Can, only: [can?: 3]
  import Ecto.Query

  @moduledoc """
  Plug functions for loading and authorizing resources for the current request.

  The plugs all store data in conn.assigns (in Phoenix applications, keys in conn.assigns can be accessed with `@key_name` in templates)

  In order to use the plug functions, you must `import Canary.Plugs`.

  You must also specify the Ecto repo to use in your configuration:
  ```
  config :canary, repo: Project.Repo
  ```
  If you wish, you may also specify the key where Canary will look for the current user record to authorize against:
  ```
  config :canary, current_user: :some_current_user
  ```

  You can specify a error handler module (in this case, `Helpers`) to be called when an action is unauthorized like so:
  ```elixir
  config :canary, error_handler: Helpers
  ```

  Module should implement the `Canary.ErrorHandler` behaviour.

  Canary will pass the `conn` to the handler function.
  """

  @doc """
  Load the given resource.

  Load the resource with id given by `conn.params["id"]` (or `conn.params[opts[:id_name]]` if `opts[:id_name]` is specified)
  and ecto model given by `opts[:model]` into `conn.assigns.resource_name`.

  `resource_name` is either inferred from the model name or specified in the plug declaration with the `:as` key.
  To infer the `resource_name`, the most specific(right most) name in the model's
  module name will be used, converted to underscore case.

  For example, `load_resource model: Some.Project.BlogPost` will load the resource into
  `conn.assigns.blog_post`

  If the resource cannot be fetched, `conn.assigns.resource_name` is set
  to nil.

  By default, when the action is `:index`, all records from the specified model will be loaded. This can
  be overridden to fetch a single record from the database by using the `:persisted` key.

  Currently, `:new` and `:create` actions are ignored, and `conn.assigns.resource_name`
  will be set to nil for these actions. This can be overridden to fetch a single record from the database
  by using the `:persisted` key.

  The `:persisted` key can override how a resource is loaded and can be useful when dealing
  with nested resources.

  Required opts:

  * `:model` - Specifies the module name of the model to load resources from

  Optional opts:

  * `:as` - Specifies the `resource_name` to use
  * `:only` - Specifies which actions to authorize
  * `:except` - Specifies which actions for which to skip authorization
  * `:preload` - Specifies association(s) to preload
  * `:id_name` - Specifies the name of the id in `conn.params`, defaults to "id"
  * `:id_field` - Specifies the name of the ID field in the database for searching :id_name value, defaults to "id".
  * `:persisted` - Specifies the resource should always be loaded from the database, defaults to false
  * `:required` - Same as `:persisted` but with not found handler - even for :index, :new or :create action
  * `:not_found_handler` - Specify a handler function to be called if the resource is not found


  Examples:
  ```
  plug :load_resource, model: Post

  plug :load_resource, model: User, preload: :posts, as: :the_user

  plug :load_resource, model: User, only: [:index, :show], preload: :posts, as: :person

  plug :load_resource, model: User, except: [:destroy]

  plug :load_resource, model: Post, id_name: "post_id", only: [:new, :create], persisted: true

  plug :load_resource, model: Post, id_name: "slug", id_field: "slug", only: [:show], persisted: true
  ```
  """
  @spec load_resource(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
  def load_resource(conn, opts) do
    action = get_action(conn)
    validate_opts(opts)

    if action_valid?(action, opts) do
      conn
      |> do_load_resource(opts)
      |> handle_not_found(opts)
    else
      conn
    end
  end

  defp do_load_resource(conn, opts) do
    action = get_action(conn)
    is_persisted = persisted?(opts)
    validate_opts(opts)

    loaded_resource =
      cond do
        is_persisted ->
          fetch_resource(conn, opts)

        action == :index ->
          fetch_all(conn, opts)

        action in [:new, :create] ->
          nil

        true ->
          fetch_resource(conn, opts)
      end

    Plug.Conn.assign(conn, get_resource_name(action, opts), loaded_resource)
  end

  @doc """
  Authorize the current user against the calling controller.

  In order to use this function,

    1) `conn.assigns[Application.get_env(:canary, :current_user, :current_user)]` must be an ecto
    struct representing the current user

    2) `conn.private` must be a map (this should not be a problem unless you explicitly modified it)

  authorize_controller checks for the name of the current controller in one of the following places
    1) :phoenix_controller in conn.private
    2) :canary_controller in conn.assigns

  In case you are not using phoenix, make sure you set the controller name in the conn.assigns
  Note that in case neither of `:phoenix_controller` or `:canary_controller` are found the requested
    authorization won't necessarily fail, rather it will trigger a `.can?` function with a `nil` controller

  If authorization succeeds, sets `conn.assigns.authorized` to true.

  If authorization fails, sets `conn.assigns.authorized` to false.

  Optional opts:

  * `:only` - Specifies which actions to authorize
  * `:except` - Specifies which actions for which to skip authorization
  * `:unauthorized_handler` - Specify a handler function to be called if the action is unauthorized

  Examples:
  ```
  plug :authorize_controller

  plug :authorize_controller, only: [:index, :show]

  plug :authorize_controller, except: [:destroy]
  ```
  """
  @spec authorize_controller(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
  def authorize_controller(conn, opts) do
    action = get_action(conn)
    validate_opts(opts)

    if action_valid?(action, opts) do
      do_authorize_controller(conn, opts) |> handle_unauthorized(opts)
    else
      conn
    end
  end

  defp do_authorize_controller(conn, opts) do
    controller = conn.assigns[:canary_controller] || conn.private[:phoenix_controller]

    current_user_name =
      opts[:current_user] ||
        Application.get_env(:canary, :current_user, :current_user)

    current_user = Map.fetch!(conn.assigns, current_user_name)
    action = get_action(conn)

    Plug.Conn.assign(conn, :authorized, can?(current_user, action, controller))
  end

  @doc """
  Authorize the current user for the given resource.

  In order to use this function,

    1) `conn.assigns[Application.get_env(:canary, :current_user, :current_user)]` must be an ecto
    struct representing the current user

    2) `conn.private` must be a map (this should not be a problem unless you explicitly modified it)

  If authorization succeeds, sets `conn.assigns.authorized` to true.

  If authorization fails, sets `conn.assigns.authorized` to false.

  For the `:index`, `:new`, and `:create` actions, the resource in the `Canada.Can` implementation
  should be the module name of the model rather than a struct. A struct should be used instead of
  the module name only if the `:persisted` key is used and you want to override the default
  authorization behavior.  This can be useful when dealing with nested resources.

  For example:

    use
    ```
    def can?(%User{}, :index, Post), do: true
    ```
    instead of
    ```
    def can?(%User{}, :index, %Post{}), do: true
    ```

    or

    use
    ```
    def can?(%User{id: user_id}, :index, %Post{user_id: user_id}), do: true
    ```
    if you are dealing with a nested resource, such as, "/post/post_id/comments"


    You can specify additional actions for which Canary will authorize based on the model name, by passing the `non_id_actions` opt to the plug.

    For example,
    ```elixir
    plug :authorize_resource, model: Post, non_id_actions: [:find_by_name]
    ```

  Required opts:

  * `:model` - Specifies the module name of the model to authorize access to

  Optional opts:

  * `:only` - Specifies which actions to authorize
  * `:except` - Specifies which actions for which to skip authorization
  * `:preload` - Specifies association(s) to preload
  * `:id_name` - Specifies the name of the id in `conn.params`, defaults to "id"
  * `:id_field` - Specifies the name of the ID field in the database for searching :id_name value, defaults to "id".
  * `:persisted` - Specifies the resource should always be loaded from the database, defaults to false
  * `:unauthorized_handler` - Specify a handler function to be called if the action is unauthorized

  Examples:
  ```
  plug :authorize_resource, model: Post

  plug :authorize_resource, model: User, preload: :posts

  plug :authorize_resource, model: User, only: [:index, :show], preload: :posts

  plug :load_resource, model: Post, id_name: "post_id", only: [:index], persisted: true, preload: :comments

  plug :load_resource, model: Post, id_name: "slug", id_field: "slug", only: [:show], persisted: true
  ```
  """
  @spec authorize_resource(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
  def authorize_resource(conn, opts) do
    action = get_action(conn)

    if action_valid?(action, opts) do
      do_authorize_resource(conn, opts) |> handle_unauthorized(opts)
    else
      conn
    end
  end

  defp do_authorize_resource(conn, opts) do
    current_user_name =
      opts[:current_user] || Application.get_env(:canary, :current_user, :current_user)

    current_user = Map.fetch!(conn.assigns, current_user_name)
    action = get_action(conn)
    is_persisted = persisted?(opts)

    non_id_actions =
      if opts[:non_id_actions] do
        Enum.concat([:index, :new, :create], opts[:non_id_actions])
      else
        [:index, :new, :create]
      end

    resource =
      cond do
        is_persisted ->
          fetch_resource(conn, opts)

        action in non_id_actions ->
          opts[:model]

        true ->
          fetch_resource(conn, opts)
      end

    Plug.Conn.assign(conn, :authorized, can?(current_user, action, resource))
  end

  @doc """
  Authorize the given resource and then load it if
  authorization succeeds.

  If the resource cannot be loaded or authorization
  fails, conn.assigns.resource_name is set to nil.

  The result of the authorization (true/false) is
  assigned to conn.assigns.authorized.

  Also, see the documentation for load_resource/2 and
  authorize_resource/2.

  Required opts:

  * `:model` - Specifies the module name of the model to load resources from

  Optional opts:

  * `:as` - Specifies the `resource_name` to use
  * `:only` - Specifies which actions to authorize
  * `:except` - Specifies which actions for which to skip authorization
  * `:preload` - Specifies association(s) to preload
  * `:id_name` - Specifies the name of the id in `conn.params`, defaults to "id"
  * `:id_field` - Specifies the name of the ID field in the database for searching :id_name value, defaults to "id".
  * `:unauthorized_handler` - Specify a handler function to be called if the action is unauthorized
  * `:not_found_handler` - Specify a handler function to be called if the resource is not found

  Note: 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.

  Examples:
  ```
  plug :load_and_authorize_resource, model: Post

  plug :load_and_authorize_resource, model: User, preload: :posts, as: :the_user

  plug :load_and_authorize_resource, model: User, only: [:index, :show], preload: :posts, as: :person

  plug :load_and_authorize_resource, model: User, except: [:destroy]

  plug :load_and_authorize_resource, model: Post, id_name: "slug", id_field: "slug", only: [:show], persisted: true
  ```
  """
  def load_and_authorize_resource(conn, opts) do
    action = get_action(conn)

    if action_valid?(action, opts) do
      do_load_and_authorize_resource(conn, opts)
    else
      conn
    end
  end

  defp do_load_and_authorize_resource(conn, opts) do
    conn
    |> do_load_resource(opts)
    |> authorize_resource(opts)
    |> maybe_handle_not_found(opts)
    |> purge_resource_if_unauthorized(opts)
  end

  # Only try to handle 404 if the response has not been sent during authorization handling
  defp maybe_handle_not_found(%{state: :sent} = conn, _opts), do: conn
  defp maybe_handle_not_found(conn, opts), do: handle_not_found(conn, opts)

  defp purge_resource_if_unauthorized(%{assigns: %{authorized: true}} = conn, _opts),
    do: conn

  defp purge_resource_if_unauthorized(%{assigns: %{authorized: false}} = conn, opts) do
    action = get_action(conn)
    Plug.Conn.assign(conn, get_resource_name(action, opts), nil)
  end

  defp fetch_resource(conn, opts) do
    repo = Application.get_env(:canary, :repo)
    action = get_action(conn)
    field_name = Keyword.get(opts, :id_field, "id")

    get_map_args = %{String.to_atom(field_name) => get_resource_id(conn, opts)}

    case Map.fetch(conn.assigns, get_resource_name(action, opts)) do
      :error ->
        repo.get_by(opts[:model], get_map_args)
        |> preload_if_needed(repo, opts)

      {:ok, nil} ->
        repo.get_by(opts[:model], get_map_args)
        |> preload_if_needed(repo, opts)

      {:ok, resource} ->
        if resource.__struct__ == opts[:model] do
          # A resource of the type passed as opts[:model] is already loaded; do not clobber it
          resource
        else
          opts[:model]
          |> repo.get_by(get_map_args)
          |> preload_if_needed(repo, opts)
        end
    end
  end

  defp fetch_all(conn, opts) do
    repo = Application.get_env(:canary, :repo)
    action = get_action(conn)
    resource_name = get_resource_name(action, opts)

    # check if a resource is already loaded at the key
    case Map.fetch(conn.assigns, resource_name) do
      :error ->
        from(m in opts[:model]) |> select([m], m) |> repo.all |> preload_if_needed(repo, opts)

      {:ok, resources} ->
        if Enum.at(resources, 0).__struct__ == opts[:model] do
          resources
        else
          from(m in opts[:model]) |> select([m], m) |> repo.all |> preload_if_needed(repo, opts)
        end
    end
  end

  defp get_action(conn) do
    case Map.fetch(conn.assigns, :canary_action) do
      {:ok, action} -> action
      _ -> conn.private.phoenix_action
    end
  end

  defp handle_unauthorized(%{assigns: %{authorized: true}} = conn, _opts),
    do: conn

  defp handle_unauthorized(%{assigns: %{authorized: false}} = conn, opts),
    do: apply_error_handler(conn, :unauthorized_handler, opts)

  defp handle_not_found(conn, opts) do
    action = get_action(conn)

    if apply_handle_not_found?(action, conn.assigns, opts) do
      apply_error_handler(conn, :not_found_handler, opts)
    else
      conn
    end
  end
end


================================================
FILE: lib/canary/utils.ex
================================================
defmodule Canary.Utils do
  @moduledoc """
  Common utils functions for `Canary.Plugs` and `Canary.Hooks`
  """

  @doc """
  Get the resource id from the connection params

      iex> Canary.Utils.get_resource_id(%{"id" => "9"}, [])
      "9"

      iex> Canary.Utils.get_resource_id(%Plug.Conn{params: %{"custom_id" => "1"}}, id_name: "custom_id")
      "1"

      iex> Canary.Utils.get_resource_id(%{"user_id" => "7"}, id_name: "user_id")
      "7"

      iex> Canary.Utils.get_resource_id(%{"other_id" => "9"}, id_name: "id")
      nil
  """
  @moduledoc since: "2.0.0"

  @spec get_resource_id(Plug.Conn.t(), Keyword.t()) :: String.t() | nil
  def get_resource_id(%Plug.Conn{params: params}, opts) do
    get_resource_id(params, opts)
  end

  @spec get_resource_id(map(), Keyword.t()) :: String.t() | nil
  def get_resource_id(params, opts) when is_map(params) do
    case opts[:id_name] do
      nil ->
        params["id"]

      id_name ->
        params[id_name]
    end
  end

  @doc """
  Preload associations if needed
  """
  @spec preload_if_needed(nil, Ecto.Repo.t(), Keyword.t()) :: nil
  def preload_if_needed(nil, _repo, _opts), do: nil

  @spec preload_if_needed([Ecto.Schema.t()], Ecto.Repo.t(), Keyword.t()) :: [Ecto.Schema.t()]
  def preload_if_needed(records, repo, opts) do
    case opts[:preload] do
      nil ->
        records

      models ->
        repo.preload(records, models)
    end
  end

  @doc ~S"""
  Check if an action is valid based on the options.

      iex> Canary.Utils.action_valid?(:index, only: [:index, :show])
        true

      iex> Canary.Utils.action_valid?(:index, except: :index)
        false

      iex> Canary.Utils.action_valid?(:show, except: :index, only: :show)
        ** (ArgumentError) You can't use both :except and :only options
  """
  @spec action_valid?(atom, Keyword.t()) :: boolean
  def action_valid?(action, opts) do
    cond do
      Keyword.has_key?(opts, :except) && Keyword.has_key?(opts, :only) ->
        raise ArgumentError, "You can't use both :except and :only options"

      Keyword.has_key?(opts, :except) ->
        !action_exempt?(action, opts)

      Keyword.has_key?(opts, :only) ->
        action_included?(action, opts)

      true ->
        true
    end
  end

  defp action_exempt?(action, opts) do
    if is_list(opts[:except]) && action in opts[:except] do
      true
    else
      action == opts[:except]
    end
  end

  defp action_included?(action, opts) do
    if is_list(opts[:only]) && action in opts[:only] do
      true
    else
      action == opts[:only]
    end
  end

  @doc """
  Check if a key is present in a keyword list
  """
  @spec required?(Keyword.t()) :: boolean
  def required?(opts) do
    !!Keyword.get(opts, :required, true)
  end

  @doc """
  Apply the error handler to the connection or socket
  """
  @spec apply_error_handler(Plug.Conn.t(), atom, Keyword.t()) :: Plug.Conn.t()
  @spec apply_error_handler(Phoenix.LiveView.Socket.t(), atom, Keyword.t()) ::
          {:halt, Phoenix.LiveView.Socket.t()}
  def apply_error_handler(conn_or_socket, handler_key, opts) do
    get_handler(handler_key, opts)
    |> apply([conn_or_socket])
  end

  defp get_handler(handler_key, opts) do
    mod_or_mod_fun =
      Keyword.get(opts, handler_key) ||
        Application.get_env(:canary, :error_handler, Canary.DefaultHandler)

    case mod_or_mod_fun do
      {mod, fun} ->
        Function.capture(mod, fun, 1)

      mod when is_atom(mod) ->
        Function.capture(mod, handler_key, 1)

      _ ->
        raise ArgumentError, "
            Invalid error handler, expected a module or a tuple with a module and a function,
            got: #{inspect(mod_or_mod_fun)}"
    end
  end

  @doc """
  Get the resource name from the options, covert it to atom and pluralize
  it if needed - only for :index action.

  If the `:as` option is provided, it will be used as the resource name.

        iex> Canary.Utils.get_resource_name(:show, model: MyApp.Post)
        :post

        iex> Canary.Utils.get_resource_name(:index, model: MyApp.Post)
        :posts

        iex> Canary.Utils.get_resource_name(:index, model: MyApp.Post, as: :my_posts)
        :my_posts
  """
  def get_resource_name(action, opts) do
    case opts[:as] do
      nil ->
        opts[:model]
        |> Module.split()
        |> List.last()
        |> Macro.underscore()
        |> pluralize_if_needed(action, opts)
        |> String.to_atom()

      as ->
        as
    end
  end

  def persisted?(opts) do
    !!Keyword.get(opts, :persisted, false) || !!Keyword.get(opts, :required, false)
  end

  defp pluralize_if_needed(name, action, opts) do
    if action in [:index] and not persisted?(opts) do
      name <> "s"
    else
      name
    end
  end

  @doc """
  Returns the :non_id_actions option if it is present
  """
  def non_id_actions(opts) do
    if opts[:non_id_actions] do
      Enum.concat([:index, :new, :create], opts[:non_id_actions])
    else
      [:index, :new, :create]
    end
  end

  @doc """
  Check if the not_found handler should be applied for given action, assigns and options
  """
  @spec apply_handle_not_found?(action :: atom(), assigns :: map(), opts :: Keyword.t()) ::
          boolean()
  def apply_handle_not_found?(action, assigns, opts) do
    non_id_actions = non_id_actions(opts)
    is_required = required?(opts)

    resource_name = Map.get(assigns, get_resource_name(action, opts))

    if is_nil(resource_name) and (is_required or action not in non_id_actions) do
      true
    else
      false
    end
  end

  @doc false
  def validate_opts(opts) do
    opts
    |> warn_deprecated_opts()
  end

  defp warn_deprecated_opts(opts) do
    if Keyword.has_key?(opts, :persisted) do
      IO.warn("The `:persisted` option is deprecated and will be removed in Canary 2.1.0. Use `:required` instead. Check the documentation for more information.")
    end

    if Keyword.has_key?(opts, :non_id_actions) do
      IO.warn("The `:non_id_actions` option is deprecated and will be removed in Canary 2.1.0. Use separate :authorize_resource plug for non_id_actions and `:except` to exclude non_in_actions. Check the documentation for more information.")
    end

    opts
  end
end


================================================
FILE: lib/canary.ex
================================================
defmodule Canary do
  @moduledoc """
  An 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.

  `load_resource/2` and `authorize_resource/2` can be used by themselves, while `load_and_authorize_resource/2` combines them both.

  The plug functions are defined in `Canary.Plugs`

  In order to use `Canary` authorization in standard pages with plug, just `import Canary.Plugs` and use plugs, for example:

  ```elixir
  defmodule MyAppWeb.PostController do
    use MyAppWeb, :controller
    import Canary.Plugs

    plug :load_and_authorize_resource,
      model: Post,
      current_user: :current_user,
      only: [:show, :edit, :update]
  end
  ```

  The LiveView hooks are defined in `Canary.Hooks`

  In order to use `Canary` authorization in LiveView, just `use Canary.Hooks` and mount hooks, for example:

  ```elixir
  defmodule MyAppWeb.PostLive do
    use MyAppWeb, :live_view
    use Canary.Hooks

    mount_canary :load_and_authorize_resource,
      on: [:handle_params, :handle_event],
      current_user: :current_user,
      model: Post,
      only: [:show, :edit, :update]

  end
  ```

  This will attach hooks to the LiveView module with `Phoenix.LiveView.attach_hook/4`.
  In the example above hooks will be attached to `handle_params` and `handle_event` stages of the LiveView lifecycle.

  Please read the documentation for `Canary.Plugs` and `Canary.Hooks` for more information.
  """
end


================================================
FILE: mix.exs
================================================
defmodule Canary.Mixfile do
  use Mix.Project

  def project do
    [
      app: :canary,
      version: "2.0.0-dev",
      elixir: "~> 1.14",
      package: package(),
      description: """
      An authorization library to restrict what resources the current user is
      allowed to access, and load those resources for you.
      """,
      build_embedded: Mix.env() == :prod,
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      consolidate_protocols: false,
      elixirc_paths: elixirc_paths(Mix.env()),
      test_options: [docs: true],
      test_coverage: [summary: [threshold: 85], ignore_modules: coverage_ignore_modules()],
      docs: [
        extras: [
          "docs/getting-started.md",
          "README.md",
          "CHANGELOG.md",
          "docs/upgrade.md",
        ],
        groups_for_modules: [
          "Error Handler": [Canary.ErrorHandler, Canary.DefaultHandler],
        ]
      ]
    ]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  def application do
    [extra_applications: [:logger]]
  end

  defp package do
    [
      maintainers: ["Chris Kelly"],
      licenses: ["MIT"],
      links: %{"GitHub" => "https://github.com/cpjk/canary"}
    ]
  end

  defp deps do
    [
      {:ecto, ">= 1.1.0"},
      {:canada, "~> 2.0.0"},
      {:plug, "~> 1.10"},
      {:ex_doc, "~> 0.7", only: :dev},
      {:earmark, ">= 0.0.0", only: :dev},
      {:mock, ">= 0.0.0", only: :test},
      {:credo, "~> 1.0", only: [:dev, :test]},
      {:phoenix, "~> 1.6", optional: true},
      {:phoenix_live_view, "~> 0.20 or ~> 1.0", optional: true},
      {:floki, ">= 0.30.0", only: :test}
    ]
  end

  defp coverage_ignore_modules do
    [
      ~r/Canary\.HooksHelper\..*/
    ]
  end
end


================================================
FILE: test/canary/default_handler_test.exs
================================================
defmodule CustomHandlers do
  def not_found_handler(conn) do
    conn
    |> Plug.Conn.assign(:legacy_error_handler, true)
  end

  def unauthorized_handler(conn) do
    conn
    |> Plug.Conn.assign(:legacy_error_handler, true)
  end
end

defmodule DefaultHandlerTest do
  use ExUnit.Case, async: false
  import Plug.Adapters.Test.Conn, only: [conn: 4]

  describe "not_found_handler/1" do
    test "calls the global :not_found_handler for plug based authorization" do
      Application.put_env(:canary, :not_found_handler, {CustomHandlers, :not_found_handler})

      params = %{"id" => "30"}

      conn =
        conn(
          %Plug.Conn{private: %{phoenix_action: :show}},
          :get,
          "/posts/30",
          params
        )
        |> Canary.DefaultHandler.not_found_handler()

      assert conn.assigns[:legacy_error_handler] == true
      assert conn.assigns[:post] == nil
    end

    test "returns conn when error_handler is not defined" do
      Application.put_env(:canary, :not_found_handler, nil)

      params = %{"id" => "30"}

      conn =
        conn(
          %Plug.Conn{private: %{phoenix_action: :show}},
          :get,
          "/posts/30",
          params
        )
        |> Canary.DefaultHandler.not_found_handler()

      assert conn.assigns[:post] == nil
      refute conn.assigns[:legacy_error_handler] == true
    end

    test "halts the socket for liveview based authorization" do
      assert {:halt, socket} =
               %Phoenix.LiveView.Socket{assigns: %{}}
               |> Canary.DefaultHandler.not_found_handler()

      assert {:redirect, %{to: "/"}} = socket.redirected
    end
  end

  describe "unauthorized_handler/1" do
    test "calls the global :not_found_handler for plug based authorization" do
      Application.put_env(:canary, :unauthorized_handler, {CustomHandlers, :unauthorized_handler})

      params = %{"id" => "30"}

      conn =
        conn(
          %Plug.Conn{private: %{phoenix_action: :show}},
          :get,
          "/posts/30",
          params
        )
        |> Canary.DefaultHandler.unauthorized_handler()

      assert conn.assigns[:legacy_error_handler] == true
      assert conn.assigns[:post] == nil
    end

    test "returns conn when error_handler is not defined" do
      Application.put_env(:canary, :unauthorized_handler, nil)

      params = %{"id" => "30"}

      conn =
        conn(
          %Plug.Conn{private: %{phoenix_action: :show}},
          :get,
          "/posts/30",
          params
        )
        |> Canary.DefaultHandler.unauthorized_handler()

      assert conn.assigns[:post] == nil
      refute conn.assigns[:legacy_error_handler] == true
    end

    test "halts the socket for liveview based authorization" do
      assert {:halt, socket} =
               %Phoenix.LiveView.Socket{assigns: %{}}
               |> Canary.DefaultHandler.unauthorized_handler()

      assert {:redirect, %{to: "/"}} = socket.redirected
    end
  end
end


================================================
FILE: test/canary/hooks_test.exs
================================================
defmodule Canary.HooksTest do
  use ExUnit.Case, async: true

  import Phoenix.ConnTest
  import Phoenix.LiveViewTest

  alias Canary.HooksHelper.{PageLive, PostLive}
  @endpoint Canary.HooksHelper.Endpoint

  setup_all do
    Application.put_env(:canary, Canary.HooksHelper.Endpoint,
      live_view: [signing_salt: "eTh8jeshoe2Bie4e"],
      secret_key_base: String.duplicate("57689", 50)
    )

    Application.put_env(:canary, :repo, Repo)

    start_supervised!(Canary.HooksHelper.Endpoint)
    |> Process.link()

    conn =
      Plug.Test.init_test_session(build_conn(), %{})
      |> Plug.Conn.assign(:current_user, %User{id: 1})

    {:ok, conn: conn}
  end

  describe "handle_hook/2" do
    test "load_resource hook on handle_params loads resource when is available" do
      uri = "http://localhost/post"
      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post, required: false]}
      params = %{}

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])

      assert socket.assigns.post == nil

      params = %{"id" => "1"}

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])

      assert socket.assigns.post == %Post{id: 1}
    end

    test "load_resource accepts already assigned resource when it matches" do
      uri = "http://localhost/post"
      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post]}
      params = %{"id" => "2"}

      socket =
        build_socket()
        |> put_assigns(%{post: %Post{id: 2}})

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, socket])

      assert socket.assigns.post == %Post{id: 2}

      socket =
        build_socket()
        |> put_assigns(%{post: %User{id: 1}})

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, socket])

      assert socket.assigns.post == %Post{id: 2, user_id: 2}
    end

    test "load_resource hook on handle_params assigns nil when resource is not available" do
      uri = "http://localhost/post"
      params = %{"id" => "13"}

      metadata = %{
        hook: :load_resource,
        stage: :handle_params,
        opts: [model: Post]
      }

      assert {:halt, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])

      assert socket.assigns.post == nil
    end

    test "load_resource hook on handle_event" do
      metadata = %{
        hook: :load_resource,
        stage: :handle_event,
        opts: [model: Post]
      }

      params = %{"id" => "1"}

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, ["my_event", params, build_socket()])

      assert socket.assigns.post == %Post{id: 1}

      params = %{"id" => "13"}

      assert {:halt, socket} =
               Canary.Hooks.handle_hook(metadata, ["my_event", params, build_socket()])

      assert socket.assigns.post == nil
    end

    test "authorize_resource hook on handle_params" do
      uri = "http://localhost/post"
      metadata = %{hook: :authorize_resource, stage: :handle_params, opts: [model: Post, required: false]}
      params = %{}

      socket =
        build_socket()
        |> put_assigns(%{post: %Post{id: 1}, current_user: %User{id: 1}})

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, socket])

      assert socket.assigns.authorized == true

      socket =
        build_socket(:delete)
        |> put_assigns(%{post: %Post{id: 1}, current_user: %User{id: 1}})

      assert {:halt, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, socket])

      assert socket.assigns.authorized == false

      socket =
        build_socket(:create)
        |> put_assigns(%{current_user: %User{id: 1}})

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, socket])

      assert socket.assigns.authorized == true
    end

    test "authorize_resource hook on handle_event" do
      metadata = %{hook: :authorize_resource, stage: :handle_event, opts: [model: Post]}
      params = %{}

      socket =
        build_socket()
        |> put_assigns(%{post: %Post{id: 1}, current_user: %User{id: 1}})

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, ["create", params, socket])

      assert socket.assigns.authorized == true

      socket =
        build_socket(:delete)
        |> put_assigns(%{post: %Post{id: 1}, current_user: %User{id: 1}})

      assert {:halt, socket} =
               Canary.Hooks.handle_hook(metadata, ["delete", params, socket])

      assert socket.assigns.authorized == false
    end

    test "load_and_authorize_resource on handle_params" do
      uri = "http://localhost/post"

      metadata = %{
        hook: :load_and_authorize_resource,
        stage: :handle_params,
        opts: [model: Post, preload: :user]
      }

      params = %{"id" => "1"}

      socket =
        build_socket(:edit)
        |> put_assigns(%{current_user: %User{id: 1}})

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, socket])

      assert socket.assigns.post == %Post{id: 1} |> Repo.preload(:user)
      assert socket.assigns.authorized == true

      socket =
        build_socket()
        |> put_assigns(%{current_user: %User{id: 1}})

      params = %{"id" => "13"}

      assert {:halt, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, socket])

      assert socket.assigns.post == nil
      assert socket.assigns.authorized == false
    end

    test "load_and_authorize_resource on handle_event" do
      metadata = %{
        hook: :load_and_authorize_resource,
        stage: :handle_event,
        opts: [model: Post, preload: :user]
      }

      params = %{"id" => "1"}

      socket =
        build_socket()
        |> put_assigns(%{current_user: %User{id: 1}})

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, ["edit", params, socket])

      assert socket.assigns.post == %Post{id: 1} |> Repo.preload(:user)
      assert socket.assigns.authorized == true

      socket =
        build_socket()
        |> put_assigns(%{current_user: %User{id: 1}})

      params = %{"id" => "13"}

      assert {:halt, socket} =
               Canary.Hooks.handle_hook(metadata, ["update", params, socket])

      assert socket.assigns.post == nil
      assert socket.assigns.authorized == false
    end

    test "accepts :id_field to override the default id field" do
      uri = "http://localhost/post"
      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post, id_field: "slug"]}
      params = %{"id" => "slug1"}

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])

      assert socket.assigns.post == %Post{id: 1, slug: "slug1"}

      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post]}
      params = %{"id" => "1"}

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])

      assert socket.assigns.post == %Post{id: 1}
    end

    test "accepts :id_name to override the default id field" do
      uri = "http://localhost/post"
      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post, id_name: "blog_post_id"]}
      params = %{"blog_post_id" => "2"}

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])

      assert socket.assigns.post == Repo.get(Post, 2)

      params = %{"id" => "1"}
      assert {:halt, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])

      assert socket.assigns.post == nil
    end


    test "accepts :preload to preload the resource" do
      uri = "http://localhost/post"
      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post, preload: :user]}
      params = %{"id" => "1"}

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])

      assert socket.assigns.post == %Post{id: 1} |> Repo.preload(:user)

      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post]}
      params = %{"id" => "1"}

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])

      assert socket.assigns.post != %Post{id: 1} |> Repo.preload(:user)

    end

    test "accepts :as to override the default assign name" do
      uri = "http://localhost/post"
      metadata = %{hook: :load_resource, stage: :handle_params, opts: [model: Post, as: :my_post]}
      params = %{"id" => "1"}

      assert {:cont, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, build_socket()])

      assert socket.assigns.my_post == %Post{id: 1}
    end

    test "accepts :current_user to override the default subject assign name" do
      uri = "http://localhost/post"

      metadata = %{
        hook: :authorize_resource,
        stage: :handle_params,
        opts: [model: Post, current_user: :my_user]
      }

      params = %{}

      socket =
        build_socket()
        |> put_assigns(%{post: %Post{id: 1}, current_user: %User{id: 1}})

      assert {:halt, socket} =
               Canary.Hooks.handle_hook(metadata, [params, uri, socket])

      assert socket.assigns.authorized == false
    end

    test "emits a warning when the hook is not defined" do
      metadata = %{hook: :invalid_hook, stage: :handle_params, opts: [model: Post]}

      assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
               {:cont, socket} =
                 Canary.Hooks.handle_hook(metadata, [%{}, "http://localhost/post", build_socket()])

               assert_raise KeyError, ~r/key :post not found in/, fn ->
                 Map.fetch!(socket.assigns, :post)
               end
             end) =~
               "Invalid type :invalid_hook for Canary hook call. Please review defined hooks with mount_canary/2"
    end
  end

  describe "integration for :load_resource" do
    test "it loads the resource correctly", %{conn: conn} do
      {:ok, lv, _html} = live(conn, "/post/1")
      assert %{post: %Post{id: 1}} = PostLive.fetch_assigns(lv)
    end

    test "it halt the socket when the resource is required", %{conn: conn} do
      assert {:error, {:redirect, %{to: "/"}}} = live(conn, "/post/13/edit")
      assert {:error, {:redirect, %{to: "/"}}} = live(conn, "/post/15/update")
    end
  end

  describe "integration for on_mount/4" do
    test "it attaches defined hooks to the socket", %{conn: conn} do
      {:ok, lv, _html} = live(conn, "/page")

      {:ok, lifecycle} = PageLive.fetch_lifecycle(lv)

      expected_handle_params = [
        %{
          id: :handle_params_load_resource_0,
          function: &PageLive.handle_params_load_resource_0/3,
          stage: :handle_params
        },
        %{
          id: :handle_params_load_and_authorize_resource_1,
          function: &PageLive.handle_params_load_and_authorize_resource_1/3,
          stage: :handle_params
        }
      ]

      assert Enum.all?(expected_handle_params, &(&1 in lifecycle.handle_params)),
             "Expected Enum: #{inspect(lifecycle.handle_params)} \n to include: #{inspect(expected_handle_params)}"

      expected_handle_events = [
        %{
          id: :handle_event_load_and_authorize_resource_2,
          function: &PageLive.handle_event_load_and_authorize_resource_2/3,
          stage: :handle_event
        }
      ]

      assert Enum.all?(expected_handle_events, &(&1 in lifecycle.handle_event)),
             "Expected Enum: #{inspect(lifecycle.handle_event)} \n to include: #{inspect(expected_handle_events)}"
    end
  end

  describe "mount_canary/2" do
    defmodule TestLive do
      use Phoenix.LiveView
      use Canary.Hooks

      mount_canary(:load_resource,
        model: Post
      )

      mount_canary(:load_and_authorize_resource,
        on: [:handle_params, :handle_event],
        model: User,
        only: [:show]
      )

      def render(assigns) do
        ~H"""
        <div>Test</div>
        """
      end

      def mount(_params, _session, socket) do
        {:ok, socket}
      end
    end

    test "defines wrapper function for events" do
      expected_fun = [
        {:handle_params_load_resource_0, 3},
        {:handle_params_load_and_authorize_resource_1, 3},
        {:handle_event_load_and_authorize_resource_2, 3}
      ]

      assert Enum.all?(expected_fun, &(&1 in TestLive.__info__(:functions)))
    end

    test "adds on_mount hook for attaching event hooks" do
      %{lifecycle: %{mount: mount}} = TestLive.__live__()

      expected_mount = %{
        function: &Canary.Hooks.on_mount/4,
        id:
          {Canary.Hooks,
           {:initialize, TestLive,
            [
              handle_params_load_resource_0: :handle_params,
              handle_params_load_and_authorize_resource_1: :handle_params,
              handle_event_load_and_authorize_resource_2: :handle_event
            ]}},
        stage: :mount
      }

      assert Enum.any?(mount, &(&1 == expected_mount))
    end

    test "emits a warning when no valid stage is provided" do
      assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
               defmodule InvalidLive do
                 use Phoenix.LiveView
                 use Canary.Hooks

                 mount_canary(:load_resource,
                   model: Post,
                   on: [:invalid_stage]
                 )

                 def mount(_params, _session, socket) do
                   {:ok, socket}
                 end
               end
             end) =~
               "mount_canary called with empty :on stages"
    end
  end

  defp build_socket(action \\ :show) do
    %Phoenix.LiveView.Socket{assigns: %{__changed__: %{}, live_action: action}}
  end

  defp put_assigns(socket, assigns) do
    %{socket | assigns: Map.merge(socket.assigns, assigns)}
  end
end


================================================
FILE: test/canary/plugs_test.exs
================================================
defmodule Canary.PlugsTest do
  import Canary.Plugs

  import Plug.Adapters.Test.Conn, only: [conn: 4]

  use ExUnit.Case, async: true

  @moduletag timeout: 100_000_000

  Application.put_env(:canary, :repo, Repo)
  Application.delete_env(:canary, :error_handler)

  test "it loads the resource correctly" do
    opts = [model: Post]

    # when the resource with the id can be fetched
    params = %{"id" => "1"}
    conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/1", params)
    expected = %{conn | assigns: Map.put(conn.assigns, :post, %Post{id: 1})}

    assert load_resource(conn, opts) == expected

    # when a resource of the desired type is already present in conn.assigns
    # it does not clobber the old resource
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{private: %{phoenix_action: :show}, assigns: %{post: %Post{id: 2}}},
        :get,
        "/posts/1",
        params
      )

    expected = Plug.Conn.assign(conn, :post, %Post{id: 2})

    assert load_resource(conn, opts) == expected

    # when a resource of the desired type is already present in conn.assigns and the action is :index
    # it does not clobber the old resource
    params = %{}

    conn =
      conn(
        %Plug.Conn{private: %{phoenix_action: :index}, assigns: %{posts: [%Post{id: 2}]}},
        :get,
        "/posts",
        params
      )

    expected = Plug.Conn.assign(conn, :posts, [%Post{id: 2}])

    assert load_resource(conn, opts) == expected

    # when a resource of a different type is already present in conn.assigns
    # it replaces that resource with the desired resource
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{private: %{phoenix_action: :show}, assigns: %{post: %User{id: 2}}},
        :get,
        "/posts/1",
        params
      )

    expected = Plug.Conn.assign(conn, :post, %Post{id: 1})

    assert load_resource(conn, opts) == expected

    # when a resource of a different type is already present in conn.assigns and the action is :index
    # it replaces that resource with the desired resource
    params = %{}

    conn =
      conn(
        %Plug.Conn{private: %{phoenix_action: :index}, assigns: %{posts: [%User{id: 2}]}},
        :get,
        "/posts",
        params
      )

    expected = Plug.Conn.assign(conn, :posts, [%Post{id: 1}, %Post{id: 2, user_id: 2}])

    assert load_resource(conn, opts) == expected

    # when the resource with the id cannot be fetched
    params = %{"id" => "3"}
    conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/3", params)
    expected = Plug.Conn.assign(conn, :post, nil)

    assert load_resource(conn, opts) == expected

    # when the action is "index"
    params = %{}
    conn = conn(%Plug.Conn{private: %{phoenix_action: :index}}, :get, "/posts", params)
    expected = Plug.Conn.assign(conn, :posts, [%Post{id: 1}, %Post{id: 2, user_id: 2}])

    assert load_resource(conn, opts) == expected

    # when the action is "new"
    params = %{}
    conn = conn(%Plug.Conn{private: %{phoenix_action: :new}}, :get, "/posts/new", params)
    expected = Plug.Conn.assign(conn, :post, nil)

    assert load_resource(conn, opts) == expected

    # when the action is "create"
    params = %{}
    conn = conn(%Plug.Conn{private: %{phoenix_action: :create}}, :post, "/posts/create", params)
    expected = Plug.Conn.assign(conn, :post, nil)

    assert load_resource(conn, opts) == expected
  end

  test "it loads the resource correctly with opts[:id_name] specified" do
    opts = [model: Post, id_name: "post_id"]

    # when id param is correct
    params = %{"post_id" => 1}
    conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/1", params)
    expected = Plug.Conn.assign(conn, :post, %Post{id: 1})

    assert load_resource(conn, opts) == expected
  end

  test "it loads the resource correctly with opts[:id_field] specified" do
    opts = [model: Post, id_name: "slug", id_field: "slug"]

    # when slug param is correct
    params = %{"slug" => "slug1"}
    conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/slug1", params)
    expected = Plug.Conn.assign(conn, :post, %Post{id: 1, slug: "slug1"})

    assert load_resource(conn, opts) == expected
  end

  test "it loads the resource correctly with opts[:persisted] specified on :index action" do
    opts = [model: User, id_name: "user_id", persisted: true]

    params = %{"user_id" => 1}
    conn = conn(%Plug.Conn{private: %{phoenix_action: :index}}, :get, "/users/1/posts", params)
    expected = Plug.Conn.assign(conn, :user, %User{id: 1})

    assert load_resource(conn, opts) == expected
  end

  test "it loads the resource correctly with opts[:persisted] specified on :new action" do
    opts = [model: User, id_name: "user_id", persisted: true]

    params = %{"user_id" => 1}
    conn = conn(%Plug.Conn{private: %{phoenix_action: :new}}, :get, "/users/1/posts/new", params)
    expected = Plug.Conn.assign(conn, :user, %User{id: 1})

    assert load_resource(conn, opts) == expected
  end

  test "it loads the resource correctly with opts[:persisted] specified on :create action" do
    opts = [model: User, id_name: "user_id", persisted: true]

    params = %{"user_id" => "1"}
    conn = conn(%Plug.Conn{private: %{phoenix_action: :create}}, :post, "/users/1/posts", params)
    expected = Plug.Conn.assign(conn, :user, %User{id: 1})

    assert load_resource(conn, opts) == expected
  end

  test "it calls the specified action when not_found with opts[:required] specified on :new action" do
    opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}, required: true]

    params = %{"id" => "3"}

    conn =
      conn(
        %Plug.Conn{assigns: %{post: nil}, private: %{phoenix_action: :new}},
        :get,
        "/posts/3/new",
        params
      )

    expected = Helpers.not_found_handler(conn)

    assert load_resource(conn, opts) == expected
  end

  test "it calls the specified action when not_found with opts[:required] specified on :create action" do
    opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}, required: true]

    params = %{"id" => "3"}

    conn =
      conn(
        %Plug.Conn{assigns: %{post: nil}, private: %{phoenix_action: :index}},
        :post,
        "/posts/3/new",
        params
      )

    expected = Helpers.not_found_handler(conn)

    assert load_resource(conn, opts) == expected
  end

  test "it calls the specified action when not_found with opts[:required] specified on :index action" do
    opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}, required: true]

    params = %{"id" => "3"}

    conn =
      conn(
        %Plug.Conn{assigns: %{post: nil}, private: %{phoenix_action: :index}},
        :get,
        "/posts/3",
        params
      )

    expected = Helpers.not_found_handler(conn)

    assert load_resource(conn, opts) == expected
  end

  test "it authorizes the resource correctly" do
    opts = [model: Post]

    # when the action is "new"
    params = %{}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :new},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/new",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected

    # when the action is "create"
    params = %{}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :create},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/create",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected

    # when the action is "index"
    params = %{}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :index},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected

    # when the action is a phoenix action
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected

    # when the current user can access the given resource
    # and the action is specified in conn.assigns.canary_action
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show}
        },
        :get,
        "/posts/1",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected

    # when both conn.assigns.canary_action and conn.private.phoenix_action are defined
    # it uses conn.assigns.canary_action for authorization
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}, canary_action: :unauthorized}
        },
        :get,
        "/posts/1",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, false)

    assert authorize_resource(conn, opts) == expected

    # when the current user cannot access the given resource
    params = %{"id" => "2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show}
        },
        :get,
        "/posts/2",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, false)

    assert authorize_resource(conn, opts) == expected

    # when the resource of the desired type already exists in conn.assigns,
    # it authorizes for that resource
    params = %{"id" => "2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %Post{user_id: 1}}
        },
        :get,
        "/posts/2",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected

    # when the resource of a different type already exists in conn.assigns,
    # it authorizes for the desired resource
    params = %{"id" => "2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %User{}}
        },
        :get,
        "/posts/2",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, false)

    assert authorize_resource(conn, opts) == expected

    # when current_user is nil
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: nil, canary_action: :create}
        },
        :post,
        "/posts",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, false)

    assert authorize_resource(conn, opts) == expected
  end

  test "it authorizes the resource correctly when using :id_field option" do
    opts = [model: Post, id_field: "slug", id_name: "slug"]

    # when the action is "new"
    params = %{}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :new},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/new",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected

    # when the action is "create"
    params = %{}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :create},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/create",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected

    # when the action is "index"
    params = %{}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :index},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected

    # when the action is a phoenix action
    params = %{"slug" => "slug1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/slug1",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected

    # when the current user can access the given resource
    # and the action is specified in conn.assigns.canary_action
    params = %{"slug" => "slug1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show}
        },
        :get,
        "/posts/slug1",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected

    # when both conn.assigns.canary_action and conn.private.phoenix_action are defined
    # it uses conn.assigns.canary_action for authorization
    params = %{"slug" => "slug1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}, canary_action: :unauthorized}
        },
        :get,
        "/posts/slug1",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, false)

    assert authorize_resource(conn, opts) == expected

    # when the current user cannot access the given resource
    params = %{"slug" => "slug2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show}
        },
        :get,
        "/posts/slug2",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, false)

    assert authorize_resource(conn, opts) == expected

    # when the resource of the desired type already exists in conn.assigns,
    # it authorizes for that resource
    params = %{"slug" => "slug2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %Post{user_id: 1}}
        },
        :get,
        "/posts/slug2",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected

    # when the resource of a different type already exists in conn.assigns,
    # it authorizes for the desired resource
    params = %{"slug" => "slug2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %User{}}
        },
        :get,
        "/posts/slug2",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, false)

    assert authorize_resource(conn, opts) == expected

    # when current_user is nil
    params = %{"slug" => "slug1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: nil, canary_action: :create}
        },
        :post,
        "/posts",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, false)

    assert authorize_resource(conn, opts) == expected
  end

  test "it authorizes the resource correctly with opts[:persisted] specified on :index action" do
    opts = [model: Post, id_name: "post_id", persisted: true]

    params = %{"post_id" => 2}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :index},
          assigns: %{current_user: %User{id: 2}}
        },
        :get,
        "/posts/post_id/comments",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected
  end

  test "it authorizes the resource correctly with opts[:persisted] specified on :new action" do
    opts = [model: Post, id_name: "post_id", persisted: true]

    params = %{"post_id" => 2}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :new},
          assigns: %{current_user: %User{id: 2}}
        },
        :get,
        "/posts/post_id/comments/new",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected
  end

  test "it authorizes the resource correctly with opts[:persisted] specified on :create action" do
    opts = [model: Post, id_name: "post_id", persisted: true]

    params = %{"post_id" => "2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :create},
          assigns: %{current_user: %User{id: 2}}
        },
        :post,
        "/posts/post_id/comments",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected
  end

  test "it loads and authorizes the resource correctly" do
    opts = [model: Post]

    # when the current user can access the given resource
    # and the resource can be loaded
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, true)
      |> Plug.Conn.assign(:post, %Post{id: 1, user_id: 1})

    assert load_and_authorize_resource(conn, opts) == expected

    # when the current user cannot access the given resource
    params = %{"id" => "2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show}
        },
        :get,
        "/posts/2",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, false)
      |> Plug.Conn.assign(:post, nil)

    assert load_and_authorize_resource(conn, opts) == expected

    # when a resource of the desired type is already present in conn.assigns
    # it does not load a new resource
    params = %{"id" => "2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %Post{user_id: 1}}
        },
        :get,
        "/posts/2",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, true)
      |> Plug.Conn.assign(:post, %Post{user_id: 1})

    assert load_and_authorize_resource(conn, opts) == expected

    # when a resource of the a different type is already present in conn.assigns
    # it loads and authorizes for the desired resource
    params = %{"id" => "2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %User{id: 1}}
        },
        :get,
        "/posts/2",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, false)
      |> Plug.Conn.assign(:post, nil)

    assert load_and_authorize_resource(conn, opts) == expected

    # when the given resource cannot be loaded
    params = %{"id" => "3"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show}
        },
        :get,
        "/posts/1",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, false)
      |> Plug.Conn.assign(:post, nil)

    assert load_and_authorize_resource(conn, opts) == expected
  end

  test "it loads and authorizes the resource correctly when using :id_field option" do
    opts = [model: Post, id_field: "slug", id_name: "slug"]

    # when the current user can access the given resource
    # and the resource can be loaded
    params = %{"slug" => "slug1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/slug1",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, true)
      |> Plug.Conn.assign(:post, %Post{id: 1, slug: "slug1", user_id: 1})

    assert load_and_authorize_resource(conn, opts) == expected

    # when the current user cannot access the given resource
    params = %{"slug" => "slug2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show}
        },
        :get,
        "/posts/slug2",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, false)
      |> Plug.Conn.assign(:post, nil)

    assert load_and_authorize_resource(conn, opts) == expected

    # when a resource of the desired type is already present in conn.assigns
    # it does not load a new resource
    params = %{"slug" => "slug2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %Post{user_id: 1}}
        },
        :get,
        "/posts/slug2",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, true)
      |> Plug.Conn.assign(:post, %Post{user_id: 1})

    assert load_and_authorize_resource(conn, opts) == expected

    # when a resource of the a different type is already present in conn.assigns
    # it loads and authorizes for the desired resource
    params = %{"slug" => "slug2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %User{id: 1}}
        },
        :get,
        "/posts/slug2",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, false)
      |> Plug.Conn.assign(:post, nil)

    assert load_and_authorize_resource(conn, opts) == expected

    # when the given resource cannot be loaded
    params = %{"slug" => "slug3"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{current_user: %User{id: 1}, canary_action: :show}
        },
        :get,
        "/posts/slug3",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, false)
      |> Plug.Conn.assign(:post, nil)

    assert load_and_authorize_resource(conn, opts) == expected
  end

  test "it loads and authorizes the resource correctly with opts[:persisted] specified on :index action" do
    opts = [model: Post, id_name: "post_id", persisted: true]

    params = %{"post_id" => 2}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :index},
          assigns: %{current_user: %User{id: 2}}
        },
        :get,
        "/posts/2/comments",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, true)
      |> Plug.Conn.assign(:post, %Post{id: 2, user_id: 2})

    assert load_and_authorize_resource(conn, opts) == expected
  end

  test "it loads and authorizes the resource correctly with opts[:persisted] specified on :new action" do
    opts = [model: Post, id_name: "post_id", persisted: true]

    params = %{"post_id" => 2}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :new},
          assigns: %{current_user: %User{id: 2}}
        },
        :get,
        "/posts/2/comments/new",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, true)
      |> Plug.Conn.assign(:post, %Post{id: 2, user_id: 2})

    assert load_and_authorize_resource(conn, opts) == expected
  end

  test "it loads and authorizes the resource correctly with opts[:persisted] specified on :create action" do
    opts = [model: Post, id_name: "post_id", persisted: true]

    params = %{"post_id" => "2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :create},
          assigns: %{current_user: %User{id: 2}}
        },
        :create,
        "/posts/2/comments",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, true)
      |> Plug.Conn.assign(:post, %Post{id: 2, user_id: 2})

    assert load_and_authorize_resource(conn, opts) == expected
  end

  test "it only loads the resource when the action is in opts[:only]" do
    # when the action is in opts[:only]
    opts = [model: Post, only: :show]
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected = Plug.Conn.assign(conn, :post, %Post{id: 1})

    assert load_resource(conn, opts) == expected

    # when the action is not opts[:only]
    opts = [model: Post, only: :other]
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected = conn

    assert load_resource(conn, opts) == expected
  end

  test "it only authorizes actions in opts[:only]" do
    # when the action is in opts[:only]
    opts = [model: Post, only: :show]
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)
    assert authorize_resource(conn, opts) == expected

    # when the action is not opts[:only]
    opts = [model: Post, only: :other]
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected = conn

    assert authorize_resource(conn, opts) == expected
  end

  test "it only loads and authorizes the resource for actions in opts[:only]" do
    # when the action is in opts[:only]
    opts = [model: Post, only: :show]
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, true)
      |> Plug.Conn.assign(:post, %Post{id: 1, user_id: 1})

    assert load_and_authorize_resource(conn, opts) == expected

    # when the action is not opts[:only]
    opts = [model: Post, only: :other]
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected = conn

    assert load_and_authorize_resource(conn, opts) == expected
  end

  test "it raises when both opts[:only] and opts[:except] are specified" do
    # when the plug is load_resource
    opts = [model: Post, only: :show, except: :index]
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected = conn

    assert_raise ArgumentError, fn ->
      load_resource(conn, opts) == expected
    end

    # when the plug is authorize_resource
    opts = [model: Post, only: :show, except: :index]
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected = conn


    assert_raise ArgumentError, fn ->
      authorize_resource(conn, opts) == expected
    end

    # when the plug is load_and_authorize_resource
    opts = [model: Post, only: :show, except: :index]
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected = conn

    assert_raise ArgumentError, fn ->
      load_and_authorize_resource(conn, opts) == expected
    end
  end

  test "it correctly skips authorization for exempt actions" do
    # when the action is exempt
    opts = [model: Post, except: :show]
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected = conn

    assert authorize_resource(conn, opts) == expected

    # when the action is not exempt
    opts = [model: Post]
    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected
  end

  test "it correctly skips loading resources for exempt actions" do
    # when the action is exempt
    opts = [model: Post, except: :show]
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected = conn
    assert load_resource(conn, opts) == expected

    # when the action is not exempt
    opts = [model: Post]
    expected = Plug.Conn.assign(conn, :post, %Post{id: 1, user_id: 1})
    assert load_resource(conn, opts) == expected
  end

  test "it correctly skips load_and_authorize_resource for exempt actions" do
    # when the action is exempt
    opts = [model: Post, except: :show]
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected = conn
    assert load_and_authorize_resource(conn, opts) == expected

    # when the action is not exempt
    opts = [model: Post]

    expected =
      conn
      |> Plug.Conn.assign(:authorized, true)
      |> Plug.Conn.assign(:post, %Post{id: 1, user_id: 1})

    assert load_and_authorize_resource(conn, opts) == expected
  end

  test "it loads the resource into a key specified by the :as option" do
    opts = [model: Post, as: :some_key]

    # when the resource with the id can be fetched
    params = %{"id" => "1"}
    conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/1", params)
    expected = Plug.Conn.assign(conn, :some_key, %Post{id: 1})

    assert load_resource(conn, opts) == expected
  end

  test "it authorizes the resource correctly when the :as key is specified" do
    opts = [model: Post, as: :some_key]

    # when the action is "new"
    params = %{}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :new},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/new",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_resource(conn, opts) == expected
    # need to check that it works for authorization as well, and for load_and_authorize_resource
  end

  test "it loads and authorizes the resource correctly when the :as key is specified" do
    opts = [model: Post, as: :some_key]

    # when the current user can access the given resource
    # and the resource can be loaded
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, true)
      |> Plug.Conn.assign(:some_key, %Post{id: 1, user_id: 1})

    assert load_and_authorize_resource(conn, opts) == expected
  end

  test "when the :as key is not specified, it loads the resource into a key inferred from the model name" do
    opts = [model: Post]

    # when the resource with the id can be fetched
    params = %{"id" => "1"}
    conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/1", params)
    expected = Plug.Conn.assign(conn, :post, %Post{id: 1})

    assert load_resource(conn, opts) == expected
  end

  test "when unauthorized, it calls the specified action" do
    opts = [model: Post, unauthorized_handler: {Helpers, :unauthorized_handler}]

    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{assigns: %{current_user: %User{id: 2}}, private: %{phoenix_action: :show}},
        :get,
        "/posts/1",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, false)
      |> Helpers.unauthorized_handler()

    assert authorize_resource(conn, opts) == expected
  end

  test "when not_found, it calls the specified action" do
    opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}]

    params = %{"id" => "3"}

    conn =
      conn(
        %Plug.Conn{assigns: %{post: nil}, private: %{phoenix_action: :show}},
        :get,
        "/posts/3",
        params
      )

    expected = Helpers.not_found_handler(conn)

    assert load_resource(conn, opts) == expected
  end

  test "when unauthorized and resource not found, it calls the specified authorization handler first" do
    opts = [
      model: Post,
      not_found_handler: {Helpers, :not_found_handler},
      unauthorized_handler: {Helpers, :unauthorized_handler}
    ]

    params = %{"id" => "3"}

    conn =
      conn(
        %Plug.Conn{assigns: %{current_user: %User{id: 2}}, private: %{phoenix_action: :show}},
        :get,
        "/posts/3",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, false)
      |> Plug.Conn.assign(:post, nil)
      |> Helpers.unauthorized_handler()

    assert load_and_authorize_resource(conn, opts) == expected
  end

  test "when the authorization handler does not halt the request, it calls the not found handler if specified" do
    opts = [
      model: Post,
      not_found_handler: {Helpers, :not_found_handler},
      unauthorized_handler: {Helpers, :non_halting_unauthorized_handler}
    ]

    params = %{"id" => "3"}

    conn =
      conn(
        %Plug.Conn{assigns: %{current_user: %User{id: 2}}, private: %{phoenix_action: :show}},
        :get,
        "/posts/3",
        params
      )

    expected =
      conn
      |> Plug.Conn.assign(:authorized, false)
      |> Plug.Conn.assign(:post, nil)
      |> Helpers.non_halting_unauthorized_handler()
      |> Helpers.not_found_handler()

    assert load_and_authorize_resource(conn, opts) == expected
  end

  defmodule UnauthorizedHandlerConfigured do
    use ExUnit.Case, async: false

    test "when unauthorized, it calls the configured action" do
      Application.put_env(:canary, :unauthorized_handler, {Helpers, :unauthorized_handler})
      opts = [model: Post]

      params = %{"id" => "1"}

      conn =
        conn(
          %Plug.Conn{assigns: %{current_user: %User{id: 2}}, private: %{phoenix_action: :show}},
          :get,
          "/posts/1",
          params
        )

      expected =
        conn
        |> Plug.Conn.assign(:authorized, false)
        |> Helpers.unauthorized_handler()

      assert authorize_resource(conn, opts) == expected
    end

    test "when unauthorized and resource not found, it calls the configured authorization handler first" do
      Application.put_env(:canary, :unauthorized_handler, {Helpers, :unauthorized_handler})
      opts = [model: Post]

      params = %{"id" => "3"}

      conn =
        conn(
          %Plug.Conn{assigns: %{current_user: %User{id: 2}}, private: %{phoenix_action: :show}},
          :get,
          "/posts/3",
          params
        )

      expected =
        conn
        |> Plug.Conn.assign(:authorized, false)
        |> Plug.Conn.assign(:post, nil)
        |> Helpers.unauthorized_handler()

      assert load_and_authorize_resource(conn, opts) == expected
    end
  end

  defmodule UnauthorizedHandlerConfiguredAndSpecified do
    use ExUnit.Case, async: false

    test "when unauthorized, it calls the opt-specified action rather than the configured action" do
      # should not be called
      Application.put_env(:canary, :unauthorized_handler, {Helpers, :does_not_exist})
      opts = [model: Post, unauthorized_handler: {Helpers, :unauthorized_handler}]

      params = %{"id" => "1"}

      conn =
        conn(
          %Plug.Conn{assigns: %{current_user: %User{id: 2}}, private: %{phoenix_action: :show}},
          :get,
          "/posts/1",
          params
        )

      expected =
        conn
        |> Helpers.unauthorized_handler()
        |> Plug.Conn.assign(:authorized, false)

      assert authorize_resource(conn, opts) == expected
    end
  end

  defmodule NotFoundHandlerConfigured do
    use ExUnit.Case, async: false

    test "when not_found, it calls the configured action" do
      Application.put_env(:canary, :error_handler, Helpers)
      opts = [model: Post]

      params = %{"id" => "4"}
      conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/4", params)

      expected =
        conn
        |> Helpers.not_found_handler()
        |> Plug.Conn.assign(:post, nil)

      assert load_resource(conn, opts) == expected
    end
  end

  defmodule NotFoundHandlerConfiguredAndSpecified do
    use ExUnit.Case, async: false

    test "when not_found, it calls the opt-specified action rather than the configured action" do
      # should not be called
      Application.put_env(:canary, :not_found_handler, {Helpers, :does_not_exist})
      opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}]

      params = %{"id" => "4"}
      conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/4", params)

      expected =
        conn
        |> Helpers.not_found_handler()
        |> Plug.Conn.assign(:post, nil)

      assert load_resource(conn, opts) == expected
    end
  end

  defmodule CurrentUser do
    use ExUnit.Case, async: true

    defmodule ApplicationConfig do
      use ExUnit.Case, async: false
      import Mock

      test_with_mock "it uses the current_user name configured", Application, [:passthrough],
        get_env: fn _, _, _ -> :current_admin end do
        # when the user configured with opts
        opts = [model: Post, except: :show]
        params = %{"id" => "1"}

        conn =
          conn(
            %Plug.Conn{
              private: %{phoenix_action: :show},
              assigns: %{current_admin: %User{id: 1}}
            },
            :get,
            "/posts/1",
            params
          )

        expected = conn

        assert authorize_resource(conn, opts) == expected
      end
    end

    test "it uses the current_user name in options" do
      # when the user configured with opts
      opts = [model: Post, current_user: :user]
      params = %{"id" => "1"}

      conn =
        conn(
          %Plug.Conn{
            private: %{phoenix_action: :show},
            assigns: %{user: %User{id: 1}, authorized: true}
          },
          :get,
          "/posts/1",
          params
        )

      expected = conn

      assert authorize_resource(conn, opts) == expected
    end

    test "it throws an error when the wrong current_user name is used" do
      # when the user configured with opts
      opts = [model: Post, current_user: :configured_current_user]
      params = %{"id" => "1"}

      conn =
        conn(
          %Plug.Conn{
            private: %{phoenix_action: :show},
            assigns: %{user: %User{id: 1}, authorized: true}
          },
          :get,
          "/posts/1",
          params
        )

      assert_raise KeyError, ~r/^key :configured_current_user not found in: %{/, fn ->
        authorize_resource(conn, opts)
      end
    end
  end

  defmodule Preload do
    use ExUnit.Case, async: true

    test "it loads the resource correctly when the :preload key is specified" do
      opts = [model: Post, preload: :user]

      # when the resource with the id can be fetched and the association exists
      params = %{"id" => "2"}
      conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/1", params)
      expected = Plug.Conn.assign(conn, :post, %Post{id: 2, user_id: 2, user: %User{id: 2}})

      assert load_resource(conn, opts) == expected

      # when the resource with the id can be fetched and the association does not exist
      params = %{"id" => "1"}
      conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/1", params)
      expected = Plug.Conn.assign(conn, :post, %Post{id: 1, user_id: 1, user: %User{id: 1}})

      assert load_resource(conn, opts) == expected

      # when the resource with the id cannot be fetched
      params = %{"id" => "3"}
      conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/3", params)
      expected = Plug.Conn.assign(conn, :post, nil)

      assert load_resource(conn, opts) == expected

      # when the action is "index"
      params = %{}
      conn = conn(%Plug.Conn{private: %{phoenix_action: :index}}, :get, "/posts", params)

      expected =
        Plug.Conn.assign(conn, :posts, [
          %Post{id: 1},
          %Post{id: 2, user_id: 2, user: %User{id: 2}}
        ])

      assert load_resource(conn, opts) == expected

      # when the action is "new"
      params = %{}
      conn = conn(%Plug.Conn{private: %{phoenix_action: :new}}, :get, "/posts/new", params)
      expected = Plug.Conn.assign(conn, :post, nil)

      assert load_resource(conn, opts) == expected
    end

    test "it authorizes the resource correctly when the :preload key is specified" do
      opts = [model: Post, preload: :user]

      # when the action is "edit"
      params = %{"id" => "2"}

      conn =
        conn(
          %Plug.Conn{
            private: %{phoenix_action: :edit},
            assigns: %{current_user: %User{id: 2}}
          },
          :get,
          "/posts/edit/2",
          params
        )

      expected = Plug.Conn.assign(conn, :authorized, true)

      assert authorize_resource(conn, opts) == expected

      # when the action is "index"
      params = %{}

      conn =
        conn(
          %Plug.Conn{
            private: %{phoenix_action: :index},
            assigns: %{current_user: %User{id: 1}}
          },
          :get,
          "/posts",
          params
        )

      expected = Plug.Conn.assign(conn, :authorized, true)

      assert authorize_resource(conn, opts) == expected
    end

    test "it loads and authorizes the resource correctly when the :preload key is specified" do
      opts = [model: Post, preload: :user]

      # when the current user can access the given resource
      # and the resource can be loaded and the association exists
      params = %{"id" => "2"}

      conn =
        conn(
          %Plug.Conn{
            private: %{phoenix_action: :show},
            assigns: %{current_user: %User{id: 2}}
          },
          :get,
          "/posts/2",
          params
        )

      expected =
        conn
        |> Plug.Conn.assign(:authorized, true)
        |> Plug.Conn.assign(:post, %Post{id: 2, user_id: 2, user: %User{id: 2}})

     assert load_and_authorize_resource(conn, opts) == expected

      # when the current user can access the given resource
      # and the resource can be loaded and the association does not exist
      params = %{"id" => "1"}

      conn =
        conn(
          %Plug.Conn{
            private: %{phoenix_action: :show},
            assigns: %{current_user: %User{id: 1}}
          },
          :get,
          "/posts/1",
          params
        )

      expected =
        conn
        |> Plug.Conn.assign(:authorized, true)
        |> Plug.Conn.assign(:post, %Post{id: 1, user_id: 1, user: %User{id: 1}})

      assert load_and_authorize_resource(conn, opts) == expected

      # when the action is "edit"
      params = %{"id" => "2"}

      conn =
        conn(
          %Plug.Conn{
            private: %{phoenix_action: :edit},
            assigns: %{current_user: %User{id: 2}}
          },
          :get,
          "/posts/edit/2",
          params
        )

      expected =
        conn
        |> Plug.Conn.assign(:authorized, true)
        |> Plug.Conn.assign(:post, %Post{id: 2, user_id: 2, user: %User{id: 2}})

      assert load_and_authorize_resource(conn, opts) == expected
    end
  end

  defmodule NonIdActions do
    use ExUnit.Case, async: true

    test "it throws an error when the non_id_actions is not a list" do
      # when opts[:non_id_actions] is set but not as a list
      opts = [model: Post, non_id_actions: :other_action]
      params = %{"id" => "1"}

      conn =
        conn(
          %Plug.Conn{
            private: %{phoenix_action: :other_action},
            assigns: %{current_user: %User{id: 1}, authorized: true}
          },
          :get,
          "/posts/other-action",
          params
        )

      assert_raise Protocol.UndefinedError, ~r/protocol Enumerable not implemented for /, fn ->
        authorize_resource(conn, opts)
      end
    end

    test "it authorizes the resource correctly when non_id_actions is a list" do
      # when opts[:non_id_actions] is set as a list
      opts = [model: Post, non_id_actions: [:other_action]]

      params = %{"id" => "1"}

      conn =
        conn(
          %Plug.Conn{
            private: %{phoenix_action: :other_action},
            assigns: %{current_user: %User{id: 1}, authorized: true}
          },
          :get,
          "/posts/other-action",
          params
        )

      expected = Plug.Conn.assign(conn, :authorized, true)

      assert authorize_resource(conn, opts) == expected
    end
  end

  test "it authorizes the controller correctly" do
    opts = [model: Post]

    # when the action is "new"
    params = %{}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :new, phoenix_controller: Myproject.SampleController},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/new",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_controller(conn, opts) == expected

    # when the action is "create"
    params = %{}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :create, phoenix_controller: Myproject.SampleController},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/create",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_controller(conn, opts) == expected

    # when the action is "index"
    params = %{}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :index, phoenix_controller: Myproject.SampleController},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_controller(conn, opts) == expected

    # when the action is a phoenix action
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show, phoenix_controller: Myproject.SampleController},
          assigns: %{current_user: %User{id: 1}}
        },
        :get,
        "/posts/1",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_controller(conn, opts) == expected

    # when the current user can access the given resource
    # and the action and controller are specified in conn.assigns.canary_action
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{},
          assigns: %{
            current_user: %User{id: 1},
            canary_action: :show,
            canary_controller: Myproject.SampleController
          }
        },
        :get,
        "/posts/1",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_controller(conn, opts) == expected

    # when both conn.assigns.canary_action and conn.private.phoenix_action are defined
    # it uses conn.assigns.canary_action for authorization
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_action: :show, phoenix_controller: Myproject.SampleController},
          assigns: %{current_user: %User{id: 1}, canary_action: :unauthorized}
        },
        :get,
        "/posts/1",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, false)

    assert authorize_controller(conn, opts) == expected

    # when the current user cannot access the given action
    params = %{"id" => "2"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_controller: Myproject.SampleController},
          assigns: %{current_user: %User{id: 1}, canary_action: :someaction}
        },
        :get,
        "/posts/2",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, false)

    assert authorize_controller(conn, opts) == expected

    # when current_user is nil
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_controller: Myproject.SampleController},
          assigns: %{current_user: nil, canary_action: :create}
        },
        :post,
        "/posts",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, false)

    assert authorize_controller(conn, opts) == expected

    # when an action is restricted on a controller
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_controller: Myproject.PartialAccessController},
          assigns: %{current_user: %User{id: 1}, canary_action: :new}
        },
        :post,
        "/posts",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, false)

    assert authorize_controller(conn, opts) == expected

    # when an action is authorized on a controller
    params = %{"id" => "1"}

    conn =
      conn(
        %Plug.Conn{
          private: %{phoenix_controller: Myproject.PartialAccessController},
          assigns: %{current_user: %User{id: 1}, canary_action: :show}
        },
        :post,
        "/posts",
        params
      )

    expected = Plug.Conn.assign(conn, :authorized, true)

    assert authorize_controller(conn, opts) == expected
  end
end


================================================
FILE: test/canary/utils_test.exs
================================================
defmodule UtilsTest do
  import Canary.Utils

  use ExUnit.Case, async: true

  describe "get_resource_id/2" do
    test "returns the id from the params" do
      assert get_resource_id(%{"id" => "9"}, []) == "9"
      assert get_resource_id(%{"user_id" => "7"}, id_name: "user_id") == "7"
    end

    test "returns the id form conn.params" do
      conn = %Plug.Conn{params: %{"id" => "9"}}
      assert get_resource_id(conn, []) == "9"

      conn = %Plug.Conn{params: %{"custom_id" => "1"}}
      assert get_resource_id(conn, id_name: "custom_id") == "1"
    end

    test "returns nil if the id is not found" do
      assert get_resource_id(%{"other_id" => "9"}, id_name: "id") == nil

      conn = %Plug.Conn{params: %{"other_id" => "9"}}
      assert get_resource_id(conn, id_name: "id") == nil
    end
  end

  describe "action_valid?/2" do
    test "returns true if the action is valid" do
      assert action_valid?(:index, only: [:index, :show]) == true
      assert action_valid?(:show, except: :index) == true
    end

    test "returns false if the action is not valid" do
      assert action_valid?(:index, except: :index) == false
      assert action_valid?(:edit, only: [:index, :show]) == false
    end

    test "raise when both :only and :except are provided" do
      assert_raise ArgumentError, fn ->
        action_valid?(:index, only: [:index], except: :index)
      end
    end
  end

  test "required?/1 returns true if the resource is required" do
    assert required?(required: true) == true
    assert required?(required: false) == false
    assert required?([]) == true
  end

  describe "apply_error_handler/3" do
    defmodule CustomErrorHandler do
      @behaviour Canary.ErrorHandler

      def not_found_handler(%Plug.Conn{} = conn) do
        %{conn | assigns: %{ok_custom_not_found_handler: true}}
      end
      def unauthorized_handler(%Plug.Conn{} = conn) do
        %{conn | assigns: %{ok_custom_unauthorized_handler: true}}
      end

      def custom_handler(%Plug.Conn{} = conn) do
        %{conn | assigns: %{ok_custom_handler: true}}
      end
    end

    test "raises if the error_handler is undefined" do
      assert_raise UndefinedFunctionError, ~r/function UnknownCustomErrorHandler.wrong_function\/1 is undefined/, fn ->
        apply_error_handler(%Plug.Conn{}, :unauthorized_handler, [
          unauthorized_handler: {UnknownCustomErrorHandler, :wrong_function}
        ])
      end

      assert_raise UndefinedFunctionError, ~r/function OtherErrorHandler.custom_function\/1 is undefined/, fn ->
        apply_error_handler(%Plug.Conn{}, :unauthorized_handler, [
          unauthorized_handler: {OtherErrorHandler, :custom_function}
        ])
      end
    end

    test "raises if the error_handler is not a module" do
      Application.put_env(:canary, :error_handler, 42)

      assert_raise ArgumentError, ~r/Invalid error handler, expected a module or a tuple with a module and a function/, fn ->
        apply_error_handler(%Plug.Conn{}, :not_found_handler, [])
      end
    end

    test "allows overriding the error handler" do
      Application.put_env(:canary, :error_handler, CustomErrorHandler)

      conn = apply_error_handler(%Plug.Conn{}, :not_found_handler, [])
      assert conn.assigns[:ok_custom_not_found_handler] == true

      conn = apply_error_handler(%Plug.Conn{}, :unauthorized_handler, [])
      assert conn.assigns[:ok_custom_unauthorized_handler] == true


      conn = apply_error_handler(%Plug.Conn{}, :unauthorized_handler, [unauthorized_handler: {Canary.DefaultHandler, :unauthorized_handler}])
      assert conn.assigns[:ok_custom_unauthorized_handler] == nil

      conn = apply_error_handler(%Plug.Conn{}, :not_found_handler, [
        not_found_handler: {CustomErrorHandler, :custom_handler}
        ])
      assert conn.assigns[:ok_custom_handler] == true
    end
  end
end


================================================
FILE: test/support/endpoint.ex
================================================
defmodule Canary.HooksHelper.Endpoint do
  use Phoenix.Endpoint, otp_app: :canary

  socket "/live", Phoenix.LiveView.Socket

  plug Canary.HooksHelper.Router
end


================================================
FILE: test/support/page_live.ex
================================================
defmodule Canary.HooksHelper.PageLive do
  use Phoenix.LiveView
  use Canary.Hooks

  mount_canary :load_resource,
    model: Post,
    required: false

  mount_canary :load_and_authorize_resource,
    on: [:handle_params, :handle_event],
    model: User,
    only: [:show],
    required: false

  def render(assigns) do
    ~H"""
    <div>Page</div>
    """
  end

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  ## test helpers

  def handle_call({:run, func}, _, socket), do: func.(socket)

  def run(lv, func) do
    GenServer.call(lv.pid, {:run, func})
  end

  def fetch_lifecycle(lv) do
    run(lv, fn socket ->
      {:reply, Map.fetch(socket.private, :lifecycle), socket}
    end)
  end
end


================================================
FILE: test/support/post_live.ex
================================================
defmodule Canary.HooksHelper.PostLive do
  use Phoenix.LiveView
  use Canary.Hooks

  mount_canary :load_resource,
    model: Post,
    only: [:show]

  mount_canary :load_resource,
    model: Post,
    only: [:edit, :update]

  def render(assigns) do
    ~H"""
    <div>Post</div>
    """
  end

  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  ## test helpers

  def handle_call({:run, func}, _, socket), do: func.(socket)

  def run(lv, func) do
    GenServer.call(lv.pid, {:run, func})
  end

  def fetch_assigns(lv) do
    run(lv, fn socket ->
      {:reply, socket.assigns, socket}
    end)
  end

  def fetch_socket(lv) do
    run(lv, fn socket ->
      {:reply, socket, socket}
    end)
  end
end


================================================
FILE: test/support/router.ex
================================================
defmodule Canary.HooksHelper.Router do
  use Phoenix.Router
  import Plug.Conn
  import Phoenix.LiveView.Router

  pipeline :browser do
    plug :fetch_session
    plug :accepts, ["html"]
    plug :fetch_live_flash
  end

  scope "/" do
    pipe_through :browser

    live "/page", Canary.HooksHelper.PageLive
    live "/post", Canary.HooksHelper.PostLive
    live "/post/:id", Canary.HooksHelper.PostLive, :show
    live "/post/:id/edit", Canary.HooksHelper.PostLive, :edit
    live "/post/:id/update", Canary.HooksHelper.PostLive, :update
  end
end


================================================
FILE: test/test_helper.exs
================================================
ExUnit.start()


defmodule User do
  defstruct id: 1
end

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    # :defaults not working so define own field with default value
    belongs_to(:user, :integer, define_field: false)

    field(:user_id, :integer, default: 1)
    field(:slug, :string)
  end
end

defmodule Repo do
  def get(User, 1), do: %User{}
  def get(User, _id), do: nil

  def get(Post, 1), do: %Post{id: 1}
  def get(Post, 2), do: %Post{id: 2, user_id: 2}
  def get(Post, _), do: nil

  def all(_), do: [%Post{id: 1}, %Post{id: 2, user_id: 2}]

  def preload(%Post{id: post_id, user_id: user_id}, :user) do
    %Post{id: post_id, user_id: user_id, user: %User{id: user_id}}
  end
  #def preload(%Post{id: 2, user_id: 2}, :user), do: %Post{id: 2, user_id: 2, user: %User{id: 2}}

  def preload([%Post{id: 1}, %Post{id: 2, user_id: 2}], :user),
    do: [%Post{id: 1}, %Post{id: 2, user_id: 2, user: %User{id: 2}}]

  def preload(resources, _), do: resources

  def get_by(User, %{id: "1"}), do: %User{}
  def get_by(User, _), do: nil

  def get_by(Post, %{id: "1"}), do: %Post{id: 1}
  def get_by(Post, %{id: "2"}), do: %Post{id: 2, user_id: 2}
  def get_by(Post, %{id: _}), do: nil

  def get_by(Post, %{slug: "slug1"}), do: %Post{id: 1, slug: "slug1"}
  def get_by(Post, %{slug: "slug2"}), do: %Post{id: 2, slug: "slug2", user_id: 2}
  def get_by(Post, %{slug: _}), do: nil
end

defimpl Canada.Can, for: User do
  def can?(%User{}, action, Myproject.PartialAccessController)
      when action in [:index, :show],
      do: true

  def can?(%User{}, action, Myproject.PartialAccessController)
      when action in [:new, :create, :update, :delete],
      do: false

  def can?(%User{}, :index, Myproject.SampleController), do: true

  def can?(%User{id: _user_id}, action, Myproject.SampleController)
      when action in [:index, :show, :new, :create, :update, :delete],
      do: true

  def can?(%User{id: user_id}, action, %Post{user_id: user_id})
      when action in [:index, :show, :new, :create],
      do: true

  def can?(%User{}, :index, Post), do: true

  def can?(%User{}, action, Post)
      when action in [:new, :create, :other_action],
      do: true

  def can?(%User{id: user_id}, action, %Post{user: %User{id: user_id}})
      when action in [:edit, :update],
      do: true

  def can?(%User{}, _, _), do: false
end

defimpl Canada.Can, for: Atom do
  def can?(nil, :create, Post), do: false
  def can?(nil, :create, Myproject.SampleController), do: false
end

defmodule Helpers do
  def unauthorized_handler(conn) do
    conn
    |> Plug.Conn.resp(403, "I'm sorry Dave. I'm afraid I can't do that.")
    |> Plug.Conn.send_resp()
  end

  def not_found_handler(conn) do
    conn
    |> Map.put(:not_found_handler_called, true)
    |> Plug.Conn.resp(404, "Resource not found.")
    |> Plug.Conn.send_resp()
  end

  def non_halting_unauthorized_handler(conn) do
    conn
  end
end

defmodule ErrorHandler do
  @behaviour Canary.ErrorHandler

  def not_found_handler(%Plug.Conn{} = conn) do
    Helpers.not_found_handler(conn)
  end

  def not_found_handler(%Phoenix.LiveView.Socket{} = socket) do
    {:halt, Phoenix.LiveView.redirect(socket, to: "/")}
  end

  def unauthorized_handler(%Plug.Conn{} = conn) do
    Helpers.unauthorized_handler(conn)
  end

  def unauthorized_handler(%Phoenix.LiveView.Socket{} = socket) do
    {:halt, Phoenix.LiveView.redirect(socket, to: "/")}
  end
end
Download .txt
gitextract_dt_ospw4/

├── .credo.exs
├── .formatter.exs
├── .github/
│   └── workflows/
│       └── elixir.yml
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config/
│   └── config.exs
├── docs/
│   ├── getting-started.md
│   └── upgrade.md
├── lib/
│   ├── canary/
│   │   ├── default_handler.ex
│   │   ├── error_handler.ex
│   │   ├── hooks.ex
│   │   ├── plugs.ex
│   │   └── utils.ex
│   └── canary.ex
├── mix.exs
└── test/
    ├── canary/
    │   ├── default_handler_test.exs
    │   ├── hooks_test.exs
    │   ├── plugs_test.exs
    │   └── utils_test.exs
    ├── support/
    │   ├── endpoint.ex
    │   ├── page_live.ex
    │   ├── post_live.ex
    │   └── router.ex
    └── test_helper.exs
Download .txt
SYMBOL INDEX (112 symbols across 15 files)

FILE: lib/canary.ex
  class Canary (line 1) | defmodule Canary

FILE: lib/canary/default_handler.ex
  class Canary.DefaultHandler (line 1) | defmodule Canary.DefaultHandler
    method not_found_handler (line 23) | def not_found_handler(%Plug.Conn{} = conn) do
    method not_found_handler (line 29) | def not_found_handler(%Phoenix.LiveView.Socket{} = socket) do
    method unauthorized_handler (line 40) | def unauthorized_handler(%Plug.Conn{} = conn) do
    method unauthorized_handler (line 46) | def unauthorized_handler(%Phoenix.LiveView.Socket{} = socket) do

FILE: lib/canary/error_handler.ex
  class Canary.ErrorHandler (line 1) | defmodule Canary.ErrorHandler

FILE: lib/canary/plugs.ex
  class Canary.Plugs (line 1) | defmodule Canary.Plugs
    method load_resource (line 91) | def load_resource(conn, opts) do
    method do_load_resource (line 104) | defp do_load_resource(conn, opts) do
    method authorize_controller (line 165) | def authorize_controller(conn, opts) do
    method do_authorize_controller (line 176) | defp do_authorize_controller(conn, opts) do
    method authorize_resource (line 263) | def authorize_resource(conn, opts) do
    method do_authorize_resource (line 273) | defp do_authorize_resource(conn, opts) do
    method load_and_authorize_resource (line 347) | def load_and_authorize_resource(conn, opts) do
    method do_load_and_authorize_resource (line 357) | defp do_load_and_authorize_resource(conn, opts) do
    method maybe_handle_not_found (line 366) | defp maybe_handle_not_found(%{state: :sent} = conn, _opts), do: conn
    method maybe_handle_not_found (line 367) | defp maybe_handle_not_found(conn, opts), do: handle_not_found(conn, opts)
    method purge_resource_if_unauthorized (line 369) | defp purge_resource_if_unauthorized(%{assigns: %{authorized: true}} = ...
    method purge_resource_if_unauthorized (line 372) | defp purge_resource_if_unauthorized(%{assigns: %{authorized: false}} =...
    method fetch_resource (line 377) | defp fetch_resource(conn, opts) do
    method fetch_all (line 405) | defp fetch_all(conn, opts) do
    method get_action (line 424) | defp get_action(conn) do
    method handle_unauthorized (line 431) | defp handle_unauthorized(%{assigns: %{authorized: true}} = conn, _opts),
    method handle_unauthorized (line 434) | defp handle_unauthorized(%{assigns: %{authorized: false}} = conn, opts),
    method handle_not_found (line 437) | defp handle_not_found(conn, opts) do

FILE: lib/canary/utils.ex
  class Canary.Utils (line 1) | defmodule Canary.Utils
    method get_resource_id (line 24) | def get_resource_id(%Plug.Conn{params: params}, opts) do
    method preload_if_needed (line 43) | def preload_if_needed(nil, _repo, _opts), do: nil
    method preload_if_needed (line 46) | def preload_if_needed(records, repo, opts) do
    method action_valid? (line 69) | def action_valid?(action, opts) do
    method action_exempt? (line 85) | defp action_exempt?(action, opts) do
    method action_included? (line 93) | defp action_included?(action, opts) do
    method required? (line 105) | def required?(opts) do
    method apply_error_handler (line 115) | def apply_error_handler(conn_or_socket, handler_key, opts) do
    method get_handler (line 120) | defp get_handler(handler_key, opts) do
    method get_resource_name (line 154) | def get_resource_name(action, opts) do
    method persisted? (line 169) | def persisted?(opts) do
    method pluralize_if_needed (line 173) | defp pluralize_if_needed(name, action, opts) do
    method non_id_actions (line 184) | def non_id_actions(opts) do
    method apply_handle_not_found? (line 197) | def apply_handle_not_found?(action, assigns, opts) do
    method validate_opts (line 211) | def validate_opts(opts) do
    method warn_deprecated_opts (line 216) | defp warn_deprecated_opts(opts) do

FILE: mix.exs
  class Canary.Mixfile (line 1) | defmodule Canary.Mixfile
    method project (line 4) | def project do
    method elixirc_paths (line 35) | defp elixirc_paths(:test), do: ["lib", "test/support"]
    method elixirc_paths (line 36) | defp elixirc_paths(_), do: ["lib"]
    method application (line 38) | def application do
    method package (line 42) | defp package do
    method deps (line 50) | defp deps do
    method coverage_ignore_modules (line 65) | defp coverage_ignore_modules do

FILE: test/canary/default_handler_test.exs
  class CustomHandlers (line 1) | defmodule CustomHandlers
    method not_found_handler (line 2) | def not_found_handler(conn) do
    method unauthorized_handler (line 7) | def unauthorized_handler(conn) do
  class DefaultHandlerTest (line 13) | defmodule DefaultHandlerTest

FILE: test/canary/hooks_test.exs
  class Canary.HooksTest (line 1) | defmodule Canary.HooksTest
    method build_socket (line 457) | defp build_socket(action \\ :show) do
    method put_assigns (line 461) | defp put_assigns(socket, assigns) do

FILE: test/canary/plugs_test.exs
  class Canary.PlugsTest (line 1) | defmodule Canary.PlugsTest
  class UnauthorizedHandlerConfigured (line 1390) | defmodule UnauthorizedHandlerConfigured
  class UnauthorizedHandlerConfiguredAndSpecified (line 1439) | defmodule UnauthorizedHandlerConfiguredAndSpecified
  class NotFoundHandlerConfigured (line 1466) | defmodule NotFoundHandlerConfigured
  class NotFoundHandlerConfiguredAndSpecified (line 1485) | defmodule NotFoundHandlerConfiguredAndSpecified
  class CurrentUser (line 1505) | defmodule CurrentUser
  class ApplicationConfig (line 1508) | defmodule ApplicationConfig
  class Preload (line 1578) | defmodule Preload
  class NonIdActions (line 1735) | defmodule NonIdActions

FILE: test/canary/utils_test.exs
  class UtilsTest (line 1) | defmodule UtilsTest

FILE: test/support/endpoint.ex
  class Canary.HooksHelper.Endpoint (line 1) | defmodule Canary.HooksHelper.Endpoint

FILE: test/support/page_live.ex
  class Canary.HooksHelper.PageLive (line 1) | defmodule Canary.HooksHelper.PageLive
    method render (line 15) | def render(assigns) do
    method mount (line 21) | def mount(_params, _session, socket) do
    method handle_call (line 27) | def handle_call({:run, func}, _, socket), do: func.(socket)
    method run (line 29) | def run(lv, func) do
    method fetch_lifecycle (line 33) | def fetch_lifecycle(lv) do

FILE: test/support/post_live.ex
  class Canary.HooksHelper.PostLive (line 1) | defmodule Canary.HooksHelper.PostLive
    method render (line 13) | def render(assigns) do
    method mount (line 19) | def mount(_params, _session, socket) do
    method handle_call (line 25) | def handle_call({:run, func}, _, socket), do: func.(socket)
    method run (line 27) | def run(lv, func) do
    method fetch_assigns (line 31) | def fetch_assigns(lv) do
    method fetch_socket (line 37) | def fetch_socket(lv) do

FILE: test/support/router.ex
  class Canary.HooksHelper.Router (line 1) | defmodule Canary.HooksHelper.Router

FILE: test/test_helper.exs
  class User (line 4) | defmodule User
  class Post (line 8) | defmodule Post
  class Repo (line 20) | defmodule Repo
    method get (line 21) | def get(User, 1), do: %User{}
    method get (line 22) | def get(User, _id), do: nil
    method get (line 24) | def get(Post, 1), do: %Post{id: 1}
    method get (line 25) | def get(Post, 2), do: %Post{id: 2, user_id: 2}
    method get (line 26) | def get(Post, _), do: nil
    method all (line 28) | def all(_), do: [%Post{id: 1}, %Post{id: 2, user_id: 2}]
    method preload (line 30) | def preload(%Post{id: post_id, user_id: user_id}, :user) do
    method preload (line 35) | def preload([%Post{id: 1}, %Post{id: 2, user_id: 2}], :user),
    method preload (line 38) | def preload(resources, _), do: resources
    method get_by (line 40) | def get_by(User, %{id: "1"}), do: %User{}
    method get_by (line 41) | def get_by(User, _), do: nil
    method get_by (line 43) | def get_by(Post, %{id: "1"}), do: %Post{id: 1}
    method get_by (line 44) | def get_by(Post, %{id: "2"}), do: %Post{id: 2, user_id: 2}
    method get_by (line 45) | def get_by(Post, %{id: _}), do: nil
    method get_by (line 47) | def get_by(Post, %{slug: "slug1"}), do: %Post{id: 1, slug: "slug1"}
    method get_by (line 48) | def get_by(Post, %{slug: "slug2"}), do: %Post{id: 2, slug: "slug2", us...
    method get_by (line 49) | def get_by(Post, %{slug: _}), do: nil
  class Helpers (line 89) | defmodule Helpers
    method unauthorized_handler (line 90) | def unauthorized_handler(conn) do
    method not_found_handler (line 96) | def not_found_handler(conn) do
    method non_halting_unauthorized_handler (line 103) | def non_halting_unauthorized_handler(conn) do
  class ErrorHandler (line 108) | defmodule ErrorHandler
    method not_found_handler (line 111) | def not_found_handler(%Plug.Conn{} = conn) do
    method not_found_handler (line 115) | def not_found_handler(%Phoenix.LiveView.Socket{} = socket) do
    method unauthorized_handler (line 119) | def unauthorized_handler(%Plug.Conn{} = conn) do
    method unauthorized_handler (line 123) | def unauthorized_handler(%Phoenix.LiveView.Socket{} = socket) do
Condensed preview — 27 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (181K chars).
[
  {
    "path": ".credo.exs",
    "chars": 5436,
    "preview": "# This file contains the configuration for Credo and you are probably reading\n# this after creating it with `mix credo.g"
  },
  {
    "path": ".formatter.exs",
    "chars": 148,
    "preview": "[\n  import_deps: [:plug, :phoenix, :phoenix_live_view],\n  inputs: [\n    \"lib/**/*.ex\",\n    \"config/*.exs\",\n    \"test/**/"
  },
  {
    "path": ".github/workflows/elixir.yml",
    "chars": 1223,
    "preview": "name: CI\n\non:\n  push:\n  pull_request:\n    branches:\n      - master\n\njobs:\n  mix_test:\n    name: mix test (OTP ${{matrix."
  },
  {
    "path": ".gitignore",
    "chars": 80,
    "preview": "/_build\n/deps\nerl_crash.dump\n*.ez\ntags\n/doc\n/cover\n*.beam\n.history\n.tool-version"
  },
  {
    "path": ".travis.yml",
    "chars": 503,
    "preview": "language: elixir\nelixir:\n  - 1.8\n  - 1.7\n  - 1.6\n  - 1.5\n  - 1.4\n  \notp_release:\n  - 21.1\n  - 20.3\n  - 19.3\n  - 18.3\n\nma"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 4265,
    "preview": "## Changelog\n\n## v2.0.0-dev\n  Canary 2.0.0 introduces authorization hooks for Phoenix LiveView. The Plug based authoriza"
  },
  {
    "path": "LICENSE",
    "chars": 1062,
    "preview": "Copyright (c) 2016 Chris Kelly\n\n\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this s"
  },
  {
    "path": "README.md",
    "chars": 13989,
    "preview": "Canary\n======\n[![Actions Status](https://github.com/cpjk/canary/workflows/CI/badge.svg)](https://github.com/runhyve/cana"
  },
  {
    "path": "config/config.exs",
    "chars": 1029,
    "preview": "# This file is responsible for configuring your application\n# and its dependencies with the aid of the Mix.Config module"
  },
  {
    "path": "docs/getting-started.md",
    "chars": 21133,
    "preview": "# Getting Started\n\nThis guide introduces **Canary**, an authorization library for **Elixir** applications using `Plug` a"
  },
  {
    "path": "docs/upgrade.md",
    "chars": 1790,
    "preview": "# Upgrade guides\n\n## Upgrading from Canary 1.2.0 to 2.0.0\n\n### Update Your Non-ID Actions\n\n> Since 2.0.0, the `:persiste"
  },
  {
    "path": "lib/canary/default_handler.ex",
    "chars": 1715,
    "preview": "defmodule Canary.DefaultHandler do\n  @moduledoc \"\"\"\n  The fallback Canary handler.\n\n  This module is used primarily as a"
  },
  {
    "path": "lib/canary/error_handler.ex",
    "chars": 599,
    "preview": "defmodule Canary.ErrorHandler do\n  @moduledoc \"\"\"\n  Specifies the behavior for handling errors in Canary.\n\n\n  \"\"\"\n  @mod"
  },
  {
    "path": "lib/canary/hooks.ex",
    "chars": 17181,
    "preview": "if Code.ensure_loaded?(Phoenix.LiveView) do\n  defmodule Canary.Hooks do\n    @moduledoc \"\"\"\n\n    Hooks functions for load"
  },
  {
    "path": "lib/canary/plugs.ex",
    "chars": 14955,
    "preview": "defmodule Canary.Plugs do\n  import Canary.Utils\n  import Canada.Can, only: [can?: 3]\n  import Ecto.Query\n\n  @moduledoc \""
  },
  {
    "path": "lib/canary/utils.ex",
    "chars": 6220,
    "preview": "defmodule Canary.Utils do\n  @moduledoc \"\"\"\n  Common utils functions for `Canary.Plugs` and `Canary.Hooks`\n  \"\"\"\n\n  @doc "
  },
  {
    "path": "lib/canary.ex",
    "chars": 1555,
    "preview": "defmodule Canary do\n  @moduledoc \"\"\"\n  An authorization library in Elixir for `Plug` and `Phoenix.LiveView` applications"
  },
  {
    "path": "mix.exs",
    "chars": 1788,
    "preview": "defmodule Canary.Mixfile do\n  use Mix.Project\n\n  def project do\n    [\n      app: :canary,\n      version: \"2.0.0-dev\",\n  "
  },
  {
    "path": "test/canary/default_handler_test.exs",
    "chars": 2972,
    "preview": "defmodule CustomHandlers do\n  def not_found_handler(conn) do\n    conn\n    |> Plug.Conn.assign(:legacy_error_handler, tru"
  },
  {
    "path": "test/canary/hooks_test.exs",
    "chars": 14183,
    "preview": "defmodule Canary.HooksTest do\n  use ExUnit.Case, async: true\n\n  import Phoenix.ConnTest\n  import Phoenix.LiveViewTest\n\n "
  },
  {
    "path": "test/canary/plugs_test.exs",
    "chars": 51303,
    "preview": "defmodule Canary.PlugsTest do\n  import Canary.Plugs\n\n  import Plug.Adapters.Test.Conn, only: [conn: 4]\n\n  use ExUnit.Cas"
  },
  {
    "path": "test/canary/utils_test.exs",
    "chars": 3873,
    "preview": "defmodule UtilsTest do\n  import Canary.Utils\n\n  use ExUnit.Case, async: true\n\n  describe \"get_resource_id/2\" do\n    test"
  },
  {
    "path": "test/support/endpoint.ex",
    "chars": 163,
    "preview": "defmodule Canary.HooksHelper.Endpoint do\n  use Phoenix.Endpoint, otp_app: :canary\n\n  socket \"/live\", Phoenix.LiveView.So"
  },
  {
    "path": "test/support/page_live.ex",
    "chars": 721,
    "preview": "defmodule Canary.HooksHelper.PageLive do\n  use Phoenix.LiveView\n  use Canary.Hooks\n\n  mount_canary :load_resource,\n    m"
  },
  {
    "path": "test/support/post_live.ex",
    "chars": 725,
    "preview": "defmodule Canary.HooksHelper.PostLive do\n  use Phoenix.LiveView\n  use Canary.Hooks\n\n  mount_canary :load_resource,\n    m"
  },
  {
    "path": "test/support/router.ex",
    "chars": 551,
    "preview": "defmodule Canary.HooksHelper.Router do\n  use Phoenix.Router\n  import Plug.Conn\n  import Phoenix.LiveView.Router\n\n  pipel"
  },
  {
    "path": "test/test_helper.exs",
    "chars": 3435,
    "preview": "ExUnit.start()\n\n\ndefmodule User do\n  defstruct id: 1\nend\n\ndefmodule Post do\n  use Ecto.Schema\n\n  schema \"posts\" do\n    #"
  }
]

About this extraction

This page contains the full source code of the cpjk/canary GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 27 files (168.6 KB), approximately 44.8k tokens, and a symbol index with 112 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!