├── test ├── test_helper.exs └── plug_test.exs ├── .travis.yml ├── .gitignore ├── lib ├── canary.ex └── canary │ └── plugs.ex ├── mix.exs ├── config └── config.exs ├── LICENSE ├── mix.lock ├── CHANGELOG.md └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.3.0 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | tags 6 | /doc 7 | *.beam 8 | -------------------------------------------------------------------------------- /lib/canary.ex: -------------------------------------------------------------------------------- 1 | defmodule Canary do 2 | @moduledoc """ 3 | Canary provides plugs to use for authorization. The plug functions are defined in `Canary.Plugs` 4 | 5 | In order to use the plugs, just `import Canary.Plugs` 6 | """ 7 | end 8 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Canary.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :canary, 6 | version: "0.14.1", 7 | elixir: "~> 1.2", 8 | package: package, 9 | description: """ 10 | An authorization library to restrict what resources the current user is 11 | allowed to access, and load resources for you. 12 | """, 13 | build_embedded: Mix.env == :prod, 14 | start_permanent: Mix.env == :prod, 15 | deps: deps, 16 | consolidate_protocols: false, 17 | docs: [extras: ["README.md"]]] 18 | end 19 | 20 | def application do 21 | [applications: [:logger]] 22 | end 23 | 24 | defp package do 25 | [maintainers: ["Chris Kelly"], 26 | licenses: ["MIT"], 27 | links: %{"GitHub" => "https://github.com/cpjk/canary"}] 28 | end 29 | 30 | defp deps do 31 | [ 32 | {:ecto, "~> 2.0"}, 33 | {:canada, "~> 1.0.0"}, 34 | {:plug, "~> 1.0"}, 35 | {:ex_doc, "~> 0.7", only: :dev}, 36 | {:earmark, ">= 0.0.0", only: :dev}, 37 | {:mock, ">= 0.0.0", only: :test} 38 | ] 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Chris Kelly 2 | 3 | 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"canada": {:hex, :canada, "1.0.0", "08f94ffff9bd4ca25f64e135ee88fac553898096e6367c217dfebbbaea66e1fa", [:mix], []}, 2 | "decimal": {:hex, :decimal, "1.1.2", "79a769d4657b2d537b51ef3c02d29ab7141d2b486b516c109642d453ee08e00c", [:mix], []}, 3 | "earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], []}, 4 | "ecto": {:hex, :ecto, "2.0.2", "b02331c1f20bbe944dbd33c8ecd8f1ccffecc02e344c4471a891baf3a25f5406", [:mix], [{:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, optional: true]}, {:mariaex, "~> 0.7.7", [hex: :mariaex, optional: true]}, {:postgrex, "~> 0.11.2", [hex: :postgrex, optional: true]}, {:db_connection, "~> 1.0-rc.2", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}]}, 5 | "ex_doc": {:hex, :ex_doc, "0.11.5", "0dc51cb84f8312162a2313d6c71573a9afa332333d8a332bb12540861b9834db", [:mix], [{:earmark, "~> 0.1.17 or ~> 0.2", [hex: :earmark, optional: true]}]}, 6 | "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []}, 7 | "mock": {:hex, :mock, "0.1.3", "657937b03f88fce89b3f7d6becc9f1ec1ac19c71081aeb32117db9bc4d9b3980", [:mix], [{:meck, "~> 0.8.2", [hex: :meck, optional: false]}]}, 8 | "plug": {:hex, :plug, "1.1.5", "de5645c18170415a72b18cc3d215c05321ddecac27a15acb923742156e98278b", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}]}, 9 | "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []}} 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ## v0.14.1 4 | * Bug fixes 5 | * Use Macro.underscore/1 instead of Mix.Utils.underscore/1 to avoid :mix dependency on production 6 | 7 | ## v0.14.0 8 | * Enhancements 9 | * 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. 10 | * Dependency changes 11 | * Elixir ~> 1.2 is now required 12 | * Ecto ~> 1.1 is now required 13 | 14 | ## v0.13.1 15 | 16 | * Enhancements 17 | * 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. 18 | * Bug Fixes 19 | * If more than one handler are specified and the first handler halts the request, the second handler will be skipped. 20 | 21 | ## v0.13.0 22 | 23 | * Enhancements 24 | * 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`. 25 | * Bug Fixes 26 | * Disabled protocol consolidation in order for tests to work on Elixir 1.2 27 | 28 | ## v0.12.2 29 | 30 | * Deprecations 31 | * 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. 32 | 33 | ## v0.12.0 34 | 35 | * Enhancements 36 | * 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`. 37 | * For example, to have Canary call `Helpers.handle_unauthorized/1`: 38 | ```elixir 39 | config :canary, unauthorized_handler: {Helpers, :handle_unauthorized} 40 | ``` 41 | * You can also specify the `:unauthorized_handler` on an individual basis by specifying the `:unauthorized_handler` `opt` in the plug call like so: 42 | ```elixir 43 | plug :load_and_authorize_resource Post, unauthorized_handler: {Helpers, :handle_unauthorized} 44 | ``` 45 | 46 | ## v0.11.0 47 | 48 | * Enhancements 49 | * 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 50 | ```elixir 51 | plug :load_and_authorize_resource, model: Post, id_name: "post_id", persisted: true, only: [:create] 52 | ``` 53 | 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` 54 | 55 | ## v0.10.0 56 | 57 | * Bug fix 58 | * Correctly checks `conn.assigns` for pre-existing resource 59 | 60 | * Deprecations 61 | * 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 62 | 63 | * Enhancements 64 | * The name of the id in `conn.params` can now be specified with the `id_name` opt 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Canary 2 | ====== 3 | [![Build Status](https://travis-ci.org/cpjk/canary.svg?branch=master 4 | "Build Status")](https://travis-ci.org/cpjk/canary) 5 | [![Hex pm](https://img.shields.io/hexpm/v/canary.svg?style=flat)](https://hex.pm/packages/canary) 6 | 7 | An authorization library in Elixir for Plug applications that restricts what resources 8 | the current user is allowed to access, and automatically loads resources for the current request. 9 | 10 | Inspired by [CanCan](https://github.com/CanCanCommunity/cancancan) for Ruby on Rails. 11 | 12 | [Read the docs](http://hexdocs.pm/canary) 13 | 14 | ## Installation 15 | 16 | For the latest master: 17 | 18 | ```elixir 19 | defp deps do 20 | {:canary, github: "cpjk/canary"} 21 | end 22 | ``` 23 | 24 | For the latest release: 25 | 26 | ```elixir 27 | defp deps do 28 | {:canary, "~> 0.14.1"} 29 | end 30 | ``` 31 | 32 | Then run `mix deps.get` to fetch the dependencies. 33 | 34 | ## Usage 35 | 36 | Canary provides three functions to be used as plugs to load and authorize resources: 37 | 38 | `load_resource/2`, `authorize_resource/2`, and `load_and_authorize_resource/2`. 39 | 40 | Just `import Canary.Plugs` in order to use the plugs. In a Phoenix app the best place would probably be in your `web/web.ex`. 41 | 42 | By default, Canary expects `conn.assigns.current_user` to contain an Ecto record representing the user to authorize. 43 | 44 | Specify your Ecto repo in your configuration: 45 | 46 | ```elixir 47 | config :canary, repo: Project.Repo 48 | ``` 49 | 50 | ### load_resource/2 51 | 52 | Loads the resource having the id given in `conn.params["id"]` from the database using the given Ecto repo and model, and assigns the resource to `conn.assigns.`, where `resource_name` is inferred from the model name. 53 | 54 | For example, 55 | 56 | ```elixir 57 | plug :load_resource, model: Project.User 58 | ``` 59 | Will load the `Project.User` having the id given in `conn.params["id"]` through `Project.Repo`, into 60 | `conn.assigns.user` 61 | 62 | ### authorize_resource/2 63 | 64 | Checks whether or not the `current_user` can perform the given action on the given resource and assigns the result (true/false) to `conn.assigns.authorized`. It is up to you to decide what to do with the result. 65 | 66 | For Phoenix applications, Canary determines the action automatically. 67 | 68 | For non-Phoenix applications, or to override the action provided by Phoenix, simply ensure that `conn.assigns.canary_action` contains an atom specifying the action. 69 | 70 | 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). 71 | 72 | ### load_and_authorize_resource/2 73 | 74 | Authorizes the resource and then loads it if authorization succeeds. Again, the resource is loaded into `conn.assigns.`. 75 | 76 | In the following example, the `User` with the same id as the `current_user` is only loaded if authorization succeeds. 77 | 78 | ### Example 79 | 80 | Let's say you have a Phoenix application with a `User` model, and you want to authorize the `current_user` for accessing `User` resources. 81 | 82 | Let's suppose that you have implemented Canada.Can in your `abilities.ex` like so: 83 | 84 | ```elixir 85 | defimpl Canada.Can, for: User do 86 | def can?(%User{ id: user_id }, action, %User{ id: user_id }) 87 | when action in [:show], do: true 88 | 89 | def can?(%User{ id: user_id }, _, _), do: false 90 | end 91 | ``` 92 | and in your `web/router.ex:` you have: 93 | 94 | ```elixir 95 | get "/users/:id", UserController, :show 96 | delete "/users/:id", UserController, :delete 97 | ``` 98 | 99 | To automatically load and authorize the `Project.User` having the `id` given in the params, you would plug your `UserController` like so: 100 | 101 | ```elixir 102 | plug :load_and_authorize_resource, model: Project.User 103 | ``` 104 | 105 | In this case, the `Project.User` specified by `conn.params["id"]` is loaded into `conn.assigns.user` for `GET /users/12`, but _not_ for `DELETE /users/12`. 106 | 107 | In this case, on `GET /users/12` authorization succeeds, and the `Project.User` specified by `conn.params["id]` will be loaded into `conn.assigns.user`. 108 | 109 | However, on `DELETE /users/12`, authorization fails and the resource is not loaded. 110 | 111 | ### Excluding actions 112 | 113 | To exclude an action from any of the plugs, pass the `:except` key, with a single action or list of actions. 114 | 115 | For example, 116 | 117 | Single action form: 118 | ```elixir 119 | plug :load_and_authorize_resource, model: Project.User, except: :show 120 | ``` 121 | List form: 122 | ```elixir 123 | plug :load_and_authorize_resource, model: Project.User, except: [:show, :create] 124 | ``` 125 | 126 | ### Authorizing only specific actions 127 | 128 | 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. 129 | 130 | For example, 131 | 132 | Single action form: 133 | ```elixir 134 | plug :load_and_authorize_resource, model: Project.User, only: :show 135 | ``` 136 | List form: 137 | ```elixir 138 | plug :load_and_authorize_resource, model: Project.User, only: [:show, :create] 139 | ``` 140 | 141 | Note: Passing both `:only` and `:except` to a plug is invalid. Currently, the plug will simply pass the `Conn` along unchanged. 142 | 143 | ### Overriding the default user 144 | 145 | Globally, the default key for finding the user to authorize can be set in your configuration as follows: 146 | ```elixir 147 | config :canary, current_user: :some_current_user 148 | ``` 149 | In this case, canary will look for the current user record in `conn.assigns.some_current_user`. 150 | 151 | The current user can also be overridden for individual plugs as follows: 152 | ```elixir 153 | plug :load_and_authorize_resource, model: Project.User, current_user: :current_admin 154 | ``` 155 | 156 | ### Specifying resource_name 157 | 158 | To specify the name under which the loaded resource is stored, pass the `:as` flag in the plug declaration. 159 | 160 | For example, 161 | ```elixir 162 | plug :load_and_authorize_resource, model: Project.Post, as: :new_post 163 | ``` 164 | will load the post into `conn.assigns.new_post` 165 | 166 | ### Preloading associations 167 | 168 | Associations can be preloaded with `Repo.preload` by passing the `:preload` option with the name of the association: 169 | 170 | ```elixir 171 | plug :load_and_authorize_resource, model: Project.User, preload: :posts 172 | ``` 173 | 174 | ### A note about index, new, and create actions 175 | For the `:index`, `:new`, and `:create` actions, the resource passed to the `Canada.Can` implementation 176 | should be the *module* name of the model rather than a struct. 177 | 178 | For example, when authorizing access to the `Post` resource, 179 | 180 | use 181 | 182 | ```elixir 183 | def can?(%User{}, :index, Post), do: true 184 | ``` 185 | 186 | instead of 187 | 188 | ```elixir 189 | def can?(%User{}, :index, %Post{}), do: true 190 | ``` 191 | ### Implementing Canada.Can for an anonymous user 192 | You may wish to define permissions for when there is no logged in current user (when `conn.assigns.current_user` is `nil`). 193 | In this case, you can implement `Canada.Can` for `nil` like so: 194 | ```elixir 195 | defimpl Canada.Can, for: Atom do 196 | # When the user is not logged in, authorization should always fail 197 | def can?(nil, _, _), do: false 198 | end 199 | ``` 200 | 201 | ### Nested associations 202 | Sometimes you need to load and authorize a parent resource when you have a relationship between two resources and you are 203 | creating a new one or listing all the children of that parent. By specifying the `:persisted` option with `true` 204 | you can load and/or authorize a nested resource. Specifying this option overrides the default loading behavior of the 205 | `:index`, `:new`, and `:create` actions by loading an individual resource. It also overrides the default 206 | authorization behavior of the `:index`, `:new`, and `create` actions by loading a struct instead of a module 207 | name for the call to `Canada.can?`. 208 | 209 | For example, when loading and authorizing a `Post` resource which can have one or more `Comment` resources, use 210 | 211 | ```elixir 212 | plug :load_and_authorize_resource, model: Post, id_name: "post_id", persisted: true, only: [:create] 213 | ``` 214 | 215 | to load and authorize the parent `Post` resource using the `post_id` in /posts/:post_id/comments before you 216 | create the `Comment` resource using its parent. 217 | 218 | ### Specifing database field 219 | 220 | 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. 221 | 222 | For example, if you want to access your posts using a string field called `slug`, you can use 223 | 224 | ```elixir 225 | plug :load_and_authorize_resource, model: Post, id_name: "slug", id_field: "slug" 226 | ``` 227 | 228 | to load and authorize the resource `Post` with the slug specified by `conn.params["slug"]` value. 229 | 230 | In this case in your `web/router.ex` you should have something like: 231 | 232 | ```elixir 233 | resources "/posts", PostController, param: "slug" 234 | ``` 235 | 236 | Then your URLs will look like: 237 | 238 | 239 | ``` 240 | /posts/my-new-post 241 | ``` 242 | 243 | instead of 244 | 245 | ``` 246 | /posts/1 247 | ``` 248 | 249 | 250 | ### Handling unauthorized actions 251 | By default, when an action is unauthorized, Canary simply sets `conn.assigns.authorized` to `false`. 252 | 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`. 253 | 254 | For example, to have Canary call `Helpers.handle_unauthorized/1`: 255 | 256 | ```elixir 257 | config :canary, unauthorized_handler: {Helpers, :handle_unauthorized} 258 | ``` 259 | ### Handling resource not found 260 | 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: 261 | 262 | ```elixir 263 | config :canary, not_found_handler: {Helpers, :handle_not_found} 264 | ``` 265 | 266 | You can also specify handlers on an an individual basis (which will override the corresponding configured handler, if any) by specifying the corresponding `opt` in the plug call: 267 | 268 | ```elixir 269 | plug :load_and_authorize_resource Post, 270 | unauthorized_handler: {Helpers, :handle_unauthorized}, 271 | not_found_handler: {Helpers, :handle_not_found} 272 | ``` 273 | 274 | Tip: If you wish 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: 275 | 276 | ```elixir 277 | def handle_unauthorized(conn) do 278 | conn 279 | |> put_flash(:error, "You can't access that page!") 280 | |> redirect(to: "/") 281 | |> halt 282 | end 283 | ``` 284 | 285 | 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. 286 | 287 | ## License 288 | MIT License. Copyright 2016 Chris Kelly. 289 | -------------------------------------------------------------------------------- /lib/canary/plugs.ex: -------------------------------------------------------------------------------- 1 | defmodule Canary.Plugs do 2 | import Canada.Can, only: [can?: 3] 3 | import Ecto.Query 4 | import Keyword, only: [has_key?: 2] 5 | 6 | @moduledoc """ 7 | Plug functions for loading and authorizing resources for the current request. 8 | 9 | The plugs all store data in conn.assigns (in Phoenix applications, keys in conn.assigns can be accessed with `@key_name` in templates) 10 | 11 | In order to use the plug functions, you must `use Canary`. 12 | 13 | You must also specify the Ecto repo to use in your configuration: 14 | ``` 15 | config :canary, repo: Project.Repo 16 | ``` 17 | If you wish, you may also specify the key where Canary will look for the current user record to authorize against: 18 | ``` 19 | config :canary, current_user: :some_current_user 20 | ``` 21 | 22 | You can specify a handler function (in this case, `Helpers.handle_unauthorized`) to be called when an action is unauthorized like so: 23 | ```elixir 24 | config :canary, unauthorized_handler: {Helpers, :handle_unauthorized} 25 | ``` 26 | or to handle when a resource is not found: 27 | ```elixir 28 | config :canary, not_found_handler: {Helpers, :handle_not_found} 29 | ``` 30 | Canary will pass the `conn` to the handler function. 31 | """ 32 | 33 | @doc """ 34 | Load the given resource. 35 | 36 | Load the resource with id given by `conn.params["id"]` (or `conn.params[opts[:id_name]]` if `opts[:id_name]` is specified) 37 | and ecto model given by `opts[:model]` into `conn.assigns.resource_name`. 38 | 39 | `resource_name` is either inferred from the model name or specified in the plug declaration with the `:as` key. 40 | To infer the `resource_name`, the most specific(right most) name in the model's 41 | module name will be used, converted to underscore case. 42 | 43 | For example, `load_resource model: Some.Project.BlogPost` will load the resource into 44 | `conn.assigns.blog_post` 45 | 46 | If the resource cannot be fetched, `conn.assigns.resource_name` is set 47 | to nil. 48 | 49 | By default, when the action is `:index`, all records from the specified model will be loaded. This can 50 | be overridden to fetch a single record from the database by using the `:persisted` key. 51 | 52 | Currently, `:new` and `:create` actions are ignored, and `conn.assigns.resource_name` 53 | will be set to nil for these actions. This can be overridden to fetch a single record from the database 54 | by using the `:persisted` key. 55 | 56 | The `:persisted` key can override how a resource is loaded and can be useful when dealing 57 | with nested resources. 58 | 59 | Required opts: 60 | 61 | * `:model` - Specifies the module name of the model to load resources from 62 | 63 | Optional opts: 64 | 65 | * `:as` - Specifies the `resource_name` to use 66 | * `:only` - Specifies which actions to authorize 67 | * `:except` - Specifies which actions for which to skip authorization 68 | * `:preload` - Specifies association(s) to preload 69 | * `:id_name` - Specifies the name of the id in `conn.params`, defaults to "id" 70 | * `:id_field` - Specifies the name of the ID field in the database for searching :id_name value, defaults to "id". 71 | * `:persisted` - Specifies the resource should always be loaded from the database, defaults to false 72 | * `:not_found_handler` - Specify a handler function to be called if the resource is not found 73 | 74 | Examples: 75 | ``` 76 | plug :load_resource, model: Post 77 | 78 | plug :load_resource, model: User, preload: :posts, as: :the_user 79 | 80 | plug :load_resource, model: User, only: [:index, :show], preload: :posts, as: :person 81 | 82 | plug :load_resource, model: User, except: [:destroy] 83 | 84 | plug :load_resource, model: Post, id_name: "post_id", only: [:new, :create], persisted: true 85 | 86 | plug :load_resource, model: Post, id_name: "slug", id_field: "slug", only: [:show], persisted: true 87 | ``` 88 | """ 89 | def load_resource(conn, opts) do 90 | conn 91 | |> action_valid?(opts) 92 | |> case do 93 | true -> _load_resource(conn, opts) |> handle_not_found(opts) 94 | false -> conn 95 | end 96 | end 97 | 98 | defp _load_resource(conn, opts) do 99 | action = get_action(conn) 100 | is_persisted = persisted?(opts) 101 | 102 | loaded_resource = cond do 103 | is_persisted -> 104 | fetch_resource(conn, opts) 105 | action == :index -> 106 | fetch_all(conn, opts) 107 | action in [:new, :create] -> 108 | nil 109 | true -> 110 | fetch_resource(conn, opts) 111 | end 112 | 113 | %{conn | assigns: Map.put(conn.assigns, resource_name(conn, opts), loaded_resource)} 114 | end 115 | 116 | @doc """ 117 | Authorize the current user for the given resource. 118 | 119 | In order to use this function, 120 | 121 | 1) `conn.assigns[Application.get_env(:canary, :current_user, :current_user)]` must be an ecto 122 | struct representing the current user 123 | 124 | 2) `conn.private` must be a map (this should not be a problem unless you explicitly modified it) 125 | 126 | If authorization succeeds, sets `conn.assigns.authorized` to true. 127 | 128 | If authorization fails, sets `conn.assigns.authorized` to false. 129 | 130 | For the `:index`, `:new`, and `:create` actions, the resource in the `Canada.Can` implementation 131 | should be the module name of the model rather than a struct. A struct should be used instead of 132 | the module name only if the `:persisted` key is used and you want to override the default 133 | authorization behavior. This can be useful when dealing with nested resources. 134 | 135 | For example: 136 | 137 | use 138 | ``` 139 | def can?(%User{}, :index, Post), do: true 140 | ``` 141 | instead of 142 | ``` 143 | def can?(%User{}, :index, %Post{}), do: true 144 | ``` 145 | 146 | or 147 | 148 | use 149 | ``` 150 | def can?(%User{id: user_id}, :index, %Post{user_id: user_id}), do: true 151 | ``` 152 | 153 | if you are dealing with a nested resource, such as, "/post/post_id/comments" 154 | 155 | Required opts: 156 | 157 | * `:model` - Specifies the module name of the model to authorize access to 158 | 159 | Optional opts: 160 | 161 | * `:only` - Specifies which actions to authorize 162 | * `:except` - Specifies which actions for which to skip authorization 163 | * `:preload` - Specifies association(s) to preload 164 | * `:id_name` - Specifies the name of the id in `conn.params`, defaults to "id" 165 | * `:id_field` - Specifies the name of the ID field in the database for searching :id_name value, defaults to "id". 166 | * `:persisted` - Specifies the resource should always be loaded from the database, defaults to false 167 | * `:unauthorized_handler` - Specify a handler function to be called if the action is unauthorized 168 | 169 | Examples: 170 | ``` 171 | plug :authorize_resource, model: Post 172 | 173 | plug :authorize_resource, model: User, preload: :posts 174 | 175 | plug :authorize_resource, model: User, only: [:index, :show], preload: :posts 176 | 177 | plug :load_resource, model: Post, id_name: "post_id", only: [:index], persisted: true, preload: :comments 178 | 179 | plug :load_resource, model: Post, id_name: "slug", id_field: "slug", only: [:show], persisted: true 180 | ``` 181 | """ 182 | def authorize_resource(conn, opts) do 183 | conn 184 | |> action_valid?(opts) 185 | |> case do 186 | true -> _authorize_resource(conn, opts) |> handle_unauthorized(opts) 187 | false -> conn 188 | end 189 | end 190 | 191 | defp _authorize_resource(conn, opts) do 192 | current_user_name = opts[:current_user] || Application.get_env(:canary, :current_user, :current_user) 193 | current_user = Dict.fetch! conn.assigns, current_user_name 194 | action = get_action(conn) 195 | is_persisted = persisted?(opts) 196 | 197 | resource = cond do 198 | is_persisted -> 199 | fetch_resource(conn, opts) 200 | action in [:index, :new, :create] -> 201 | opts[:model] 202 | true -> 203 | fetch_resource(conn, opts) 204 | end 205 | 206 | case current_user |> can?(action, resource) do 207 | true -> 208 | %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 209 | false -> 210 | %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 211 | end 212 | end 213 | 214 | @doc """ 215 | Authorize the given resource and then load it if 216 | authorization succeeds. 217 | 218 | If the resource cannot be loaded or authorization 219 | fails, conn.assigns.resource_name is set to nil. 220 | 221 | The result of the authorization (true/false) is 222 | assigned to conn.assigns.authorized. 223 | 224 | Also, see the documentation for load_resource/2 and 225 | authorize_resource/2. 226 | 227 | Required opts: 228 | 229 | * `:model` - Specifies the module name of the model to load resources from 230 | 231 | Optional opts: 232 | 233 | * `:as` - Specifies the `resource_name` to use 234 | * `:only` - Specifies which actions to authorize 235 | * `:except` - Specifies which actions for which to skip authorization 236 | * `:preload` - Specifies association(s) to preload 237 | * `:id_name` - Specifies the name of the id in `conn.params`, defaults to "id" 238 | * `:id_field` - Specifies the name of the ID field in the database for searching :id_name value, defaults to "id". 239 | * `:unauthorized_handler` - Specify a handler function to be called if the action is unauthorized 240 | * `:not_found_handler` - Specify a handler function to be called if the resource is not found 241 | 242 | Note: If both an `:unauthorized_handler` and a `:not_found_handler` are specified for `load_and_authorize_resource`, 243 | and the request meets the criteria for both, the `:unauthorized_handler` will be called first. 244 | 245 | Examples: 246 | ``` 247 | plug :load_and_authorize_resource, model: Post 248 | 249 | plug :load_and_authorize_resource, model: User, preload: :posts, as: :the_user 250 | 251 | plug :load_and_authorize_resource, model: User, only: [:index, :show], preload: :posts, as: :person 252 | 253 | plug :load_and_authorize_resource, model: User, except: [:destroy] 254 | 255 | plug :load_and_authorize_resource, model: Post, id_name: "slug", id_field: "slug", only: [:show], persisted: true 256 | ``` 257 | """ 258 | def load_and_authorize_resource(conn, opts) do 259 | conn 260 | |> action_valid?(opts) 261 | |> case do 262 | true -> _load_and_authorize_resource(conn, opts) 263 | false -> conn 264 | end 265 | end 266 | 267 | defp _load_and_authorize_resource(conn, opts) do 268 | conn 269 | |> Map.put(:skip_canary_handler, true) # skip not_found_handler so auth handler can catch first if needed 270 | |> load_resource(opts) 271 | |> Map.delete(:skip_canary_handler) # allow auth handling 272 | |> authorize_resource(opts) 273 | |> maybe_handle_not_found(opts) 274 | |> purge_resource_if_unauthorized(opts) 275 | end 276 | 277 | # Only try to handle 404 if the response has not been sent during authorization handling 278 | defp maybe_handle_not_found(conn = %{state: :sent}, _opts), do: conn 279 | defp maybe_handle_not_found(conn, opts), do: handle_not_found(conn, opts) 280 | 281 | defp purge_resource_if_unauthorized(conn = %{assigns: %{authorized: true}}, _opts), 282 | do: conn 283 | defp purge_resource_if_unauthorized(conn = %{assigns: %{authorized: false}}, opts), 284 | do: %{conn | assigns: Map.put(conn.assigns, resource_name(conn, opts), nil)} 285 | 286 | defp fetch_resource(conn, opts) do 287 | repo = Application.get_env(:canary, :repo) 288 | 289 | field_name = (opts[:id_field] || "id") 290 | 291 | get_map_args = %{field_name => get_resource_id(conn, opts)} 292 | get_map_args = (for {key, val} <- get_map_args, into: %{}, do: {String.to_atom(key), val}) 293 | 294 | conn.assigns 295 | |> Map.fetch(resource_name(conn, opts)) # check if a resource is already loaded at the key 296 | |> case do 297 | :error -> 298 | repo.get_by(opts[:model], get_map_args) 299 | |> preload_if_needed(repo, opts) 300 | {:ok, nil} -> 301 | repo.get_by(opts[:model], get_map_args) 302 | |> preload_if_needed(repo, opts) 303 | {:ok, resource} -> 304 | case (resource.__struct__ == opts[:model]) do 305 | true -> # A resource of the type passed as opts[:model] is already loaded; do not clobber it 306 | resource 307 | false -> 308 | repo.get_by(opts[:model], get_map_args) 309 | |> preload_if_needed(repo, opts) 310 | end 311 | end 312 | end 313 | 314 | defp fetch_all(conn, opts) do 315 | repo = Application.get_env(:canary, :repo) 316 | 317 | conn 318 | |> Map.fetch(resource_name(conn, opts)) 319 | |> case do # check if a resource is already loaded at the key 320 | :error -> 321 | from(m in opts[:model]) |> select([m], m) |> repo.all |> preload_if_needed(repo, opts) 322 | {:ok, resource} -> 323 | case (resource.__struct__ == opts[:model]) do 324 | true -> 325 | resource 326 | false -> 327 | from(m in opts[:model]) |> select([m], m) |> repo.all |> preload_if_needed(repo, opts) 328 | end 329 | end 330 | end 331 | 332 | defp get_resource_id(conn, opts) do 333 | case opts[:id_name] do 334 | nil -> 335 | conn.params["id"] 336 | id_name -> 337 | conn.params[id_name] 338 | end 339 | end 340 | 341 | defp get_action(conn) do 342 | conn.assigns 343 | |> Map.fetch(:canary_action) 344 | |> case do 345 | {:ok, action} -> action 346 | _ -> conn.private.phoenix_action 347 | end 348 | end 349 | 350 | defp action_exempt?(conn, opts) do 351 | action = get_action(conn) 352 | 353 | (is_list(opts[:except]) && action in opts[:except]) 354 | |> case do 355 | true -> true 356 | false -> action == opts[:except] 357 | end 358 | end 359 | 360 | defp action_included?(conn, opts) do 361 | action = get_action(conn) 362 | 363 | (is_list(opts[:only]) && action in opts[:only]) 364 | |> case do 365 | true -> true 366 | false -> action == opts[:only] 367 | end 368 | end 369 | 370 | defp action_valid?(conn, opts) do 371 | cond do 372 | has_key?(opts, :except) && has_key?(opts, :only) -> 373 | false 374 | has_key?(opts, :except) -> 375 | !action_exempt?(conn, opts) 376 | has_key?(opts, :only) -> 377 | action_included?(conn, opts) 378 | true -> 379 | true 380 | end 381 | end 382 | 383 | defp persisted?(opts) do 384 | !!Keyword.get(opts, :persisted, false) 385 | end 386 | 387 | defp resource_name(conn, opts) do 388 | case opts[:as] do 389 | nil -> 390 | opts[:model] 391 | |> Module.split 392 | |> List.last 393 | |> Macro.underscore 394 | |> pluralize_if_needed(conn, opts) 395 | |> String.to_atom 396 | as -> as 397 | end 398 | end 399 | 400 | defp pluralize_if_needed(name, conn, opts) do 401 | if get_action(conn) in [:index] and not persisted?(opts) do 402 | name <> "s" 403 | else 404 | name 405 | end 406 | end 407 | 408 | defp preload_if_needed(nil, _repo, _opts) do 409 | nil 410 | end 411 | 412 | defp preload_if_needed(records, repo, opts) do 413 | case opts[:preload] do 414 | nil -> 415 | records 416 | models -> 417 | repo.preload(records, models) 418 | end 419 | end 420 | 421 | defp handle_unauthorized(conn = %{skip_canary_handler: true}, _opts), 422 | do: conn 423 | defp handle_unauthorized(conn = %{assigns: %{authorized: true}}, _opts), 424 | do: conn 425 | defp handle_unauthorized(conn = %{assigns: %{authorized: false}}, opts), 426 | do: apply_error_handler(conn, :unauthorized_handler, opts) 427 | 428 | defp handle_not_found(conn = %{skip_canary_handler: true}, _opts) do 429 | conn 430 | end 431 | 432 | defp handle_not_found(conn, opts) do 433 | action = get_action(conn) 434 | 435 | case is_nil(Map.get(conn.assigns, resource_name(conn, opts))) 436 | and not action in [:index, :new, :create] do 437 | 438 | true -> apply_error_handler(conn, :not_found_handler, opts) 439 | false -> conn 440 | end 441 | end 442 | 443 | defp apply_error_handler(conn, handler_key, opts) do 444 | handler = Keyword.get(opts, handler_key) 445 | || Application.get_env(:canary, handler_key) 446 | 447 | case handler do 448 | {mod, fun} -> apply(mod, fun, [conn]) 449 | nil -> conn 450 | end 451 | end 452 | end 453 | -------------------------------------------------------------------------------- /test/plug_test.exs: -------------------------------------------------------------------------------- 1 | defmodule User do 2 | defstruct id: 1 3 | end 4 | 5 | defmodule Post do 6 | use Ecto.Model 7 | 8 | schema "posts" do 9 | belongs_to :user, :integer, define_field: false # :defaults not working so define own field with default value 10 | 11 | field :user_id, :integer, default: 1 12 | field :slug, :string 13 | end 14 | end 15 | 16 | defmodule Repo do 17 | def get(User, 1), do: %User{} 18 | def get(User, _id), do: nil 19 | 20 | def get(Post, 1), do: %Post{id: 1} 21 | def get(Post, 2), do: %Post{id: 2, user_id: 2} 22 | def get(Post, _), do: nil 23 | 24 | def all(_), do: [%Post{id: 1}, %Post{id: 2, user_id: 2}] 25 | 26 | def preload(%Post{id: 1}, :user), do: %Post{id: 1} 27 | def preload(%Post{id: 2, user_id: 2}, :user), do: %Post{id: 2, user_id: 2, user: %User{id: 2}} 28 | 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}}] 29 | def preload(resources, _), do: resources 30 | 31 | def get_by(User, %{id: 1}), do: %User{} 32 | def get_by(User, _), do: nil 33 | 34 | def get_by(Post, %{id: 1}), do: %Post{id: 1} 35 | def get_by(Post, %{id: 2}), do: %Post{id: 2, user_id: 2} 36 | def get_by(Post, %{id: _}), do: nil 37 | 38 | def get_by(Post, %{slug: "slug1"}), do: %Post{id: 1, slug: "slug1"} 39 | def get_by(Post, %{slug: "slug2"}), do: %Post{id: 2, slug: "slug2", user_id: 2} 40 | def get_by(Post, %{slug: _}), do: nil 41 | end 42 | 43 | defimpl Canada.Can, for: User do 44 | 45 | def can?(%User{id: user_id}, action, %Post{user_id: user_id}) 46 | when action in [:index, :show, :new, :create], do: true 47 | 48 | def can?(%User{}, :index, Post), do: true 49 | 50 | def can?(%User{}, action, Post) 51 | when action in [:new, :create], do: true 52 | 53 | def can?(%User{id: user_id}, action, %Post{user: %User{id: user_id}}) 54 | when action in [:edit, :update], do: true 55 | 56 | def can?(%User{}, _, _), do: false 57 | end 58 | 59 | defimpl Canada.Can, for: Atom do 60 | def can?(nil, :create, Post), do: false 61 | end 62 | 63 | defmodule Helpers do 64 | def unauthorized_handler(conn) do 65 | conn 66 | |> Map.put(:unauthorized_handler_called, true) 67 | |> Plug.Conn.resp(403, "I'm sorry Dave. I'm afraid I can't do that.") 68 | |> Plug.Conn.send_resp 69 | end 70 | 71 | def not_found_handler(conn) do 72 | conn 73 | |> Map.put(:not_found_handler_called, true) 74 | |> Plug.Conn.resp(404, "Resource not found.") 75 | |> Plug.Conn.send_resp 76 | end 77 | 78 | def non_halting_unauthorized_handler(conn) do 79 | conn 80 | |> Map.put(:unauthorized_handler_called, true) 81 | end 82 | end 83 | 84 | defmodule PlugTest do 85 | import Canary.Plugs 86 | 87 | import Plug.Adapters.Test.Conn, only: [conn: 4] 88 | 89 | use ExUnit.Case, async: true 90 | 91 | @moduletag timeout: 100000000 92 | 93 | Application.put_env :canary, :repo, Repo 94 | 95 | test "it loads the resource correctly" do 96 | opts = [model: Post] 97 | 98 | # when the resource with the id can be fetched 99 | params = %{"id" => 1} 100 | conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/1", params) 101 | expected = %{conn | assigns: Map.put(conn.assigns, :post, %Post{id: 1})} 102 | 103 | assert load_resource(conn, opts) == expected 104 | 105 | # when a resource of the desired type is already present in conn.assigns 106 | # it does not clobber the old resource 107 | params = %{"id" => 1} 108 | conn = conn(%Plug.Conn{private: %{phoenix_action: :show}, assigns: %{post: %Post{id: 2}}}, :get, "/posts/1", params) 109 | expected = %{conn | assigns: Map.put(conn.assigns, :post, %Post{id: 2})} 110 | 111 | assert load_resource(conn, opts) == expected 112 | 113 | # when a resource of a different type is already present in conn.assigns 114 | # it replaces that resource with the desired resource 115 | params = %{"id" => 1} 116 | conn = conn(%Plug.Conn{private: %{phoenix_action: :show}, assigns: %{post: %User{id: 2}}}, :get, "/posts/1", params) 117 | expected = %{conn | assigns: Map.put(conn.assigns, :post, %Post{id: 1})} 118 | 119 | assert load_resource(conn, opts) == expected 120 | 121 | # when the resource with the id cannot be fetched 122 | params = %{"id" => 3} 123 | conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/3", params) 124 | expected = %{conn | assigns: Map.put(conn.assigns, :post, nil)} 125 | 126 | assert load_resource(conn, opts) == expected 127 | 128 | 129 | # when the action is "index" 130 | params = %{} 131 | conn = conn(%Plug.Conn{private: %{phoenix_action: :index}}, :get, "/posts", params) 132 | expected = %{conn | assigns: Map.put(conn.assigns, :posts, [%Post{id: 1}, %Post{id: 2, user_id: 2}])} 133 | 134 | assert load_resource(conn, opts) == expected 135 | 136 | 137 | # when the action is "new" 138 | params = %{} 139 | conn = conn(%Plug.Conn{private: %{phoenix_action: :new}}, :get, "/posts/new", params) 140 | expected = %{conn | assigns: Map.put(conn.assigns, :post, nil)} 141 | 142 | assert load_resource(conn, opts) == expected 143 | 144 | 145 | # when the action is "create" 146 | params = %{} 147 | conn = conn(%Plug.Conn{private: %{phoenix_action: :create}}, :post, "/posts/create", params) 148 | expected = %{conn | assigns: Map.put(conn.assigns, :post, nil)} 149 | 150 | assert load_resource(conn, opts) == expected 151 | end 152 | 153 | test "it loads the resource correctly with opts[:id_name] specified" do 154 | opts = [model: Post, id_name: "post_id"] 155 | 156 | # when id param is correct 157 | params = %{"post_id" => 1} 158 | conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/1", params) 159 | expected = %{conn | assigns: Map.put(conn.assigns, :post, %Post{id: 1})} 160 | 161 | assert load_resource(conn, opts) == expected 162 | end 163 | 164 | test "it loads the resource correctly with opts[:id_field] specified" do 165 | opts = [model: Post, id_name: "slug", id_field: "slug"] 166 | 167 | # when slug param is correct 168 | params = %{"slug" => "slug1"} 169 | conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/slug1", params) 170 | expected = %{conn | assigns: Map.put(conn.assigns, :post, %Post{id: 1, slug: "slug1"})} 171 | 172 | assert load_resource(conn, opts) == expected 173 | end 174 | 175 | test "it loads the resource correctly with opts[:persisted] specified on :index action" do 176 | opts = [model: User, id_name: "user_id", persisted: true] 177 | 178 | params = %{"user_id" => 1} 179 | conn = conn(%Plug.Conn{private: %{phoenix_action: :index}}, :get, "/users/1/posts", params) 180 | expected = %{conn | assigns: Map.put(conn.assigns, :user, %User{id: 1})} 181 | 182 | assert load_resource(conn, opts) == expected 183 | end 184 | 185 | test "it loads the resource correctly with opts[:persisted] specified on :new action" do 186 | opts = [model: User, id_name: "user_id", persisted: true] 187 | 188 | params = %{"user_id" => 1} 189 | conn = conn(%Plug.Conn{private: %{phoenix_action: :new}}, :get, "/users/1/posts/new", params) 190 | expected = %{conn | assigns: Map.put(conn.assigns, :user, %User{id: 1})} 191 | 192 | assert load_resource(conn, opts) == expected 193 | end 194 | 195 | test "it loads the resource correctly with opts[:persisted] specified on :create action" do 196 | opts = [model: User, id_name: "user_id", persisted: true] 197 | 198 | params = %{"user_id" => 1} 199 | conn = conn(%Plug.Conn{private: %{phoenix_action: :create}}, :post, "/users/1/posts", params) 200 | expected = %{conn | assigns: Map.put(conn.assigns, :user, %User{id: 1})} 201 | 202 | assert load_resource(conn, opts) == expected 203 | end 204 | 205 | test "it authorizes the resource correctly" do 206 | opts = [model: Post] 207 | 208 | # when the action is "new" 209 | params = %{} 210 | conn = conn( 211 | %Plug.Conn{ 212 | private: %{phoenix_action: :new}, 213 | assigns: %{current_user: %User{id: 1}} 214 | }, 215 | :get, 216 | "/posts/new", 217 | params 218 | ) 219 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 220 | 221 | assert authorize_resource(conn, opts) == expected 222 | 223 | 224 | # when the action is "create" 225 | params = %{} 226 | conn = conn( 227 | %Plug.Conn{ 228 | private: %{phoenix_action: :create}, 229 | assigns: %{current_user: %User{id: 1}} 230 | }, 231 | :get, 232 | "/posts/create", 233 | params 234 | ) 235 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 236 | 237 | assert authorize_resource(conn, opts) == expected 238 | 239 | 240 | # when the action is "index" 241 | params = %{} 242 | conn = conn( 243 | %Plug.Conn{ 244 | private: %{phoenix_action: :index}, 245 | assigns: %{current_user: %User{id: 1}} 246 | }, 247 | :get, 248 | "/posts", 249 | params 250 | ) 251 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 252 | 253 | assert authorize_resource(conn, opts) == expected 254 | 255 | 256 | # when the action is a phoenix action 257 | params = %{"id" => 1} 258 | conn = conn( 259 | %Plug.Conn{ 260 | private: %{phoenix_action: :show}, 261 | assigns: %{current_user: %User{id: 1}} 262 | }, 263 | :get, 264 | "/posts/1", 265 | params 266 | ) 267 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 268 | 269 | assert authorize_resource(conn, opts) == expected 270 | 271 | 272 | # when the current user can access the given resource 273 | # and the action is specified in conn.assigns.canary_action 274 | params = %{"id" => 1} 275 | conn = conn( 276 | %Plug.Conn{ 277 | private: %{}, 278 | assigns: %{current_user: %User{id: 1}, canary_action: :show} 279 | }, 280 | :get, 281 | "/posts/1", 282 | params 283 | ) 284 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 285 | 286 | assert authorize_resource(conn, opts) == expected 287 | 288 | 289 | # when both conn.assigns.canary_action and conn.private.phoenix_action are defined 290 | # it uses conn.assigns.canary_action for authorization 291 | params = %{"id" => 1} 292 | conn = conn( 293 | %Plug.Conn{ 294 | private: %{phoenix_action: :show}, 295 | assigns: %{current_user: %User{id: 1}, canary_action: :unauthorized} 296 | }, 297 | :get, 298 | "/posts/1", 299 | params 300 | ) 301 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 302 | 303 | assert authorize_resource(conn, opts) == expected 304 | 305 | 306 | # when the current user cannot access the given resource 307 | params = %{"id" => 2} 308 | conn = conn( 309 | %Plug.Conn{ 310 | private: %{}, 311 | assigns: %{current_user: %User{id: 1}, canary_action: :show} 312 | }, 313 | :get, 314 | "/posts/2", 315 | params 316 | ) 317 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 318 | 319 | assert authorize_resource(conn, opts) == expected 320 | 321 | # when the resource of the desired type already exists in conn.assigns, 322 | # it authorizes for that resource 323 | params = %{"id" => 2} 324 | conn = conn( 325 | %Plug.Conn{ 326 | private: %{}, 327 | assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %Post{user_id: 1}} 328 | }, 329 | :get, 330 | "/posts/2", 331 | params 332 | ) 333 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 334 | 335 | assert authorize_resource(conn, opts) == expected 336 | 337 | # when the resource of a different type already exists in conn.assigns, 338 | # it authorizes for the desired resource 339 | params = %{"id" => 2} 340 | conn = conn( 341 | %Plug.Conn{ 342 | private: %{}, 343 | assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %User{}} 344 | }, 345 | :get, 346 | "/posts/2", 347 | params 348 | ) 349 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 350 | 351 | assert authorize_resource(conn, opts) == expected 352 | 353 | # when current_user is nil 354 | params = %{"id" => 1} 355 | conn = conn( 356 | %Plug.Conn{ 357 | private: %{}, 358 | assigns: %{current_user: nil, canary_action: :create} 359 | }, 360 | :post, 361 | "/posts", 362 | params 363 | ) 364 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 365 | 366 | assert authorize_resource(conn, opts) == expected 367 | end 368 | 369 | test "it authorizes the resource correctly when using :id_field option" do 370 | opts = [model: Post, id_field: "slug", id_name: "slug"] 371 | 372 | # when the action is "new" 373 | params = %{} 374 | conn = conn( 375 | %Plug.Conn{ 376 | private: %{phoenix_action: :new}, 377 | assigns: %{current_user: %User{id: 1}} 378 | }, 379 | :get, 380 | "/posts/new", 381 | params 382 | ) 383 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 384 | 385 | assert authorize_resource(conn, opts) == expected 386 | 387 | 388 | # when the action is "create" 389 | params = %{} 390 | conn = conn( 391 | %Plug.Conn{ 392 | private: %{phoenix_action: :create}, 393 | assigns: %{current_user: %User{id: 1}} 394 | }, 395 | :get, 396 | "/posts/create", 397 | params 398 | ) 399 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 400 | 401 | assert authorize_resource(conn, opts) == expected 402 | 403 | 404 | # when the action is "index" 405 | params = %{} 406 | conn = conn( 407 | %Plug.Conn{ 408 | private: %{phoenix_action: :index}, 409 | assigns: %{current_user: %User{id: 1}} 410 | }, 411 | :get, 412 | "/posts", 413 | params 414 | ) 415 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 416 | 417 | assert authorize_resource(conn, opts) == expected 418 | 419 | 420 | # when the action is a phoenix action 421 | params = %{"slug" => "slug1"} 422 | conn = conn( 423 | %Plug.Conn{ 424 | private: %{phoenix_action: :show}, 425 | assigns: %{current_user: %User{id: 1}} 426 | }, 427 | :get, 428 | "/posts/slug1", 429 | params 430 | ) 431 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 432 | 433 | assert authorize_resource(conn, opts) == expected 434 | 435 | 436 | # when the current user can access the given resource 437 | # and the action is specified in conn.assigns.canary_action 438 | params = %{"slug" => "slug1"} 439 | conn = conn( 440 | %Plug.Conn{ 441 | private: %{}, 442 | assigns: %{current_user: %User{id: 1}, canary_action: :show} 443 | }, 444 | :get, 445 | "/posts/slug1", 446 | params 447 | ) 448 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 449 | 450 | assert authorize_resource(conn, opts) == expected 451 | 452 | 453 | # when both conn.assigns.canary_action and conn.private.phoenix_action are defined 454 | # it uses conn.assigns.canary_action for authorization 455 | params = %{"slug" => "slug1"} 456 | conn = conn( 457 | %Plug.Conn{ 458 | private: %{phoenix_action: :show}, 459 | assigns: %{current_user: %User{id: 1}, canary_action: :unauthorized} 460 | }, 461 | :get, 462 | "/posts/slug1", 463 | params 464 | ) 465 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 466 | 467 | assert authorize_resource(conn, opts) == expected 468 | 469 | 470 | # when the current user cannot access the given resource 471 | params = %{"slug" => "slug2"} 472 | conn = conn( 473 | %Plug.Conn{ 474 | private: %{}, 475 | assigns: %{current_user: %User{id: 1}, canary_action: :show} 476 | }, 477 | :get, 478 | "/posts/slug2", 479 | params 480 | ) 481 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 482 | 483 | assert authorize_resource(conn, opts) == expected 484 | 485 | # when the resource of the desired type already exists in conn.assigns, 486 | # it authorizes for that resource 487 | params = %{"slug" => "slug2"} 488 | conn = conn( 489 | %Plug.Conn{ 490 | private: %{}, 491 | assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %Post{user_id: 1}} 492 | }, 493 | :get, 494 | "/posts/slug2", 495 | params 496 | ) 497 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 498 | 499 | assert authorize_resource(conn, opts) == expected 500 | 501 | # when the resource of a different type already exists in conn.assigns, 502 | # it authorizes for the desired resource 503 | params = %{"slug" => "slug2"} 504 | conn = conn( 505 | %Plug.Conn{ 506 | private: %{}, 507 | assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %User{}} 508 | }, 509 | :get, 510 | "/posts/slug2", 511 | params 512 | ) 513 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 514 | 515 | assert authorize_resource(conn, opts) == expected 516 | 517 | # when current_user is nil 518 | params = %{"slug" => "slug1"} 519 | conn = conn( 520 | %Plug.Conn{ 521 | private: %{}, 522 | assigns: %{current_user: nil, canary_action: :create} 523 | }, 524 | :post, 525 | "/posts", 526 | params 527 | ) 528 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 529 | 530 | assert authorize_resource(conn, opts) == expected 531 | end 532 | 533 | test "it authorizes the resource correctly with opts[:persisted] specified on :index action" do 534 | opts = [model: Post, id_name: "post_id", persisted: true] 535 | 536 | params = %{"post_id" => 2} 537 | conn = conn( 538 | %Plug.Conn{ 539 | private: %{phoenix_action: :index}, 540 | assigns: %{current_user: %User{id: 2}} 541 | }, 542 | :get, 543 | "/posts/post_id/comments", 544 | params 545 | ) 546 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 547 | 548 | assert authorize_resource(conn, opts) == expected 549 | end 550 | 551 | test "it authorizes the resource correctly with opts[:persisted] specified on :new action" do 552 | opts = [model: Post, id_name: "post_id", persisted: true] 553 | 554 | params = %{"post_id" => 2} 555 | conn = conn( 556 | %Plug.Conn{ 557 | private: %{phoenix_action: :new}, 558 | assigns: %{current_user: %User{id: 2}} 559 | }, 560 | :get, 561 | "/posts/post_id/comments/new", 562 | params 563 | ) 564 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 565 | 566 | assert authorize_resource(conn, opts) == expected 567 | end 568 | 569 | test "it authorizes the resource correctly with opts[:persisted] specified on :create action" do 570 | opts = [model: Post, id_name: "post_id", persisted: true] 571 | 572 | params = %{"post_id" => 2} 573 | conn = conn( 574 | %Plug.Conn{ 575 | private: %{phoenix_action: :create}, 576 | assigns: %{current_user: %User{id: 2}} 577 | }, 578 | :post, 579 | "/posts/post_id/comments", 580 | params 581 | ) 582 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 583 | 584 | assert authorize_resource(conn, opts) == expected 585 | end 586 | 587 | test "it loads and authorizes the resource correctly" do 588 | opts = [model: Post] 589 | 590 | # when the current user can access the given resource 591 | # and the resource can be loaded 592 | params = %{"id" => 1} 593 | conn = conn( 594 | %Plug.Conn{ 595 | private: %{phoenix_action: :show}, 596 | assigns: %{current_user: %User{id: 1}} 597 | }, 598 | :get, 599 | "/posts/1", 600 | params 601 | ) 602 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 603 | expected = %{expected | assigns: Map.put(expected.assigns, :post, %Post{id: 1, user_id: 1})} 604 | 605 | assert load_and_authorize_resource(conn, opts) == expected 606 | 607 | 608 | # when the current user cannot access the given resource 609 | params = %{"id" => 2} 610 | conn = conn( 611 | %Plug.Conn{ 612 | private: %{}, 613 | assigns: %{current_user: %User{id: 1}, canary_action: :show} 614 | }, 615 | :get, 616 | "/posts/2", 617 | params 618 | ) 619 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 620 | expected = %{expected | assigns: Map.put(expected.assigns, :post, nil)} 621 | 622 | assert load_and_authorize_resource(conn, opts) == expected 623 | 624 | # when a resource of the desired type is already present in conn.assigns 625 | # it does not load a new resource 626 | params = %{"id" => 2} 627 | conn = conn( 628 | %Plug.Conn{ 629 | private: %{}, 630 | assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %Post{user_id: 1}} 631 | }, 632 | :get, 633 | "/posts/2", 634 | params 635 | ) 636 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 637 | expected = %{expected | assigns: Map.put(expected.assigns, :post, %Post{user_id: 1})} 638 | 639 | assert load_and_authorize_resource(conn, opts) == expected 640 | 641 | # when a resource of the a different type is already present in conn.assigns 642 | # it loads and authorizes for the desired resource 643 | params = %{"id" => 2} 644 | conn = conn( 645 | %Plug.Conn{ 646 | private: %{}, 647 | assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %User{id: 1}} 648 | }, 649 | :get, 650 | "/posts/2", 651 | params 652 | ) 653 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 654 | expected = %{expected | assigns: Map.put(expected.assigns, :post, nil)} 655 | 656 | assert load_and_authorize_resource(conn, opts) == expected 657 | 658 | # when the given resource cannot be loaded 659 | params = %{"id" => 3} 660 | conn = conn( 661 | %Plug.Conn{ 662 | private: %{}, 663 | assigns: %{current_user: %User{id: 1}, canary_action: :show} 664 | }, 665 | :get, 666 | "/posts/1", 667 | params 668 | ) 669 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 670 | expected = %{expected | assigns: Map.put(expected.assigns, :post, nil)} 671 | 672 | assert load_and_authorize_resource(conn, opts) == expected 673 | end 674 | 675 | test "it loads and authorizes the resource correctly when using :id_field option" do 676 | opts = [model: Post, id_field: "slug", id_name: "slug"] 677 | 678 | # when the current user can access the given resource 679 | # and the resource can be loaded 680 | params = %{"slug" => "slug1"} 681 | conn = conn( 682 | %Plug.Conn{ 683 | private: %{phoenix_action: :show}, 684 | assigns: %{current_user: %User{id: 1}} 685 | }, 686 | :get, 687 | "/posts/slug1", 688 | params 689 | ) 690 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 691 | expected = %{expected | assigns: Map.put(expected.assigns, :post, %Post{id: 1, slug: "slug1", user_id: 1})} 692 | 693 | assert load_and_authorize_resource(conn, opts) == expected 694 | 695 | 696 | # when the current user cannot access the given resource 697 | params = %{"slug" => "slug2"} 698 | conn = conn( 699 | %Plug.Conn{ 700 | private: %{}, 701 | assigns: %{current_user: %User{id: 1}, canary_action: :show} 702 | }, 703 | :get, 704 | "/posts/slug2", 705 | params 706 | ) 707 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 708 | expected = %{expected | assigns: Map.put(expected.assigns, :post, nil)} 709 | 710 | assert load_and_authorize_resource(conn, opts) == expected 711 | 712 | # when a resource of the desired type is already present in conn.assigns 713 | # it does not load a new resource 714 | params = %{"slug" => "slug2"} 715 | conn = conn( 716 | %Plug.Conn{ 717 | private: %{}, 718 | assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %Post{user_id: 1}} 719 | }, 720 | :get, 721 | "/posts/slug2", 722 | params 723 | ) 724 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 725 | expected = %{expected | assigns: Map.put(expected.assigns, :post, %Post{user_id: 1})} 726 | 727 | assert load_and_authorize_resource(conn, opts) == expected 728 | 729 | # when a resource of the a different type is already present in conn.assigns 730 | # it loads and authorizes for the desired resource 731 | params = %{"slug" => "slug2"} 732 | conn = conn( 733 | %Plug.Conn{ 734 | private: %{}, 735 | assigns: %{current_user: %User{id: 1}, canary_action: :show, post: %User{id: 1}} 736 | }, 737 | :get, 738 | "/posts/slug2", 739 | params 740 | ) 741 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 742 | expected = %{expected | assigns: Map.put(expected.assigns, :post, nil)} 743 | 744 | assert load_and_authorize_resource(conn, opts) == expected 745 | 746 | # when the given resource cannot be loaded 747 | params = %{"slug" => "slug3"} 748 | conn = conn( 749 | %Plug.Conn{ 750 | private: %{}, 751 | assigns: %{current_user: %User{id: 1}, canary_action: :show} 752 | }, 753 | :get, 754 | "/posts/slug3", 755 | params 756 | ) 757 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 758 | expected = %{expected | assigns: Map.put(expected.assigns, :post, nil)} 759 | 760 | assert load_and_authorize_resource(conn, opts) == expected 761 | end 762 | 763 | test "it loads and authorizes the resource correctly with opts[:persisted] specified on :index action" do 764 | opts = [model: Post, id_name: "post_id", persisted: true] 765 | 766 | params = %{"post_id" => 2} 767 | conn = conn( 768 | %Plug.Conn{ 769 | private: %{phoenix_action: :index}, 770 | assigns: %{current_user: %User{id: 2}} 771 | }, 772 | :get, 773 | "/posts/2/comments", 774 | params 775 | ) 776 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 777 | expected = %{expected | assigns: Map.put(expected.assigns, :post, %Post{id: 2, user_id: 2})} 778 | 779 | assert load_and_authorize_resource(conn, opts) == expected 780 | end 781 | 782 | test "it loads and authorizes the resource correctly with opts[:persisted] specified on :new action" do 783 | opts = [model: Post, id_name: "post_id", persisted: true] 784 | 785 | params = %{"post_id" => 2} 786 | conn = conn( 787 | %Plug.Conn{ 788 | private: %{phoenix_action: :new}, 789 | assigns: %{current_user: %User{id: 2}} 790 | }, 791 | :get, 792 | "/posts/2/comments/new", 793 | params 794 | ) 795 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 796 | expected = %{expected | assigns: Map.put(expected.assigns, :post, %Post{id: 2, user_id: 2})} 797 | 798 | assert load_and_authorize_resource(conn, opts) == expected 799 | end 800 | 801 | test "it loads and authorizes the resource correctly with opts[:persisted] specified on :create action" do 802 | opts = [model: Post, id_name: "post_id", persisted: true] 803 | 804 | params = %{"post_id" => 2} 805 | conn = conn( 806 | %Plug.Conn{ 807 | private: %{phoenix_action: :create}, 808 | assigns: %{current_user: %User{id: 2}} 809 | }, 810 | :create, 811 | "/posts/2/comments", 812 | params 813 | ) 814 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 815 | expected = %{expected | assigns: Map.put(expected.assigns, :post, %Post{id: 2, user_id: 2})} 816 | 817 | assert load_and_authorize_resource(conn, opts) == expected 818 | end 819 | 820 | test "it only loads the resource when the action is in opts[:only]" do 821 | # when the action is in opts[:only] 822 | opts = [model: Post, only: :show] 823 | params = %{"id" => 1} 824 | conn = conn( 825 | %Plug.Conn{ 826 | private: %{phoenix_action: :show}, 827 | assigns: %{current_user: %User{id: 1}} 828 | }, 829 | :get, 830 | "/posts/1", 831 | params 832 | ) 833 | expected = %{conn | assigns: Map.put(conn.assigns, :post, %Post{id: 1})} 834 | 835 | assert load_resource(conn, opts) == expected 836 | 837 | 838 | # when the action is not opts[:only] 839 | opts = [model: Post, only: :other] 840 | params = %{"id" => 1} 841 | conn = conn( 842 | %Plug.Conn{ 843 | private: %{phoenix_action: :show}, 844 | assigns: %{current_user: %User{id: 1}} 845 | }, 846 | :get, 847 | "/posts/1", 848 | params 849 | ) 850 | expected = conn 851 | 852 | assert load_resource(conn, opts) == expected 853 | end 854 | 855 | test "it only authorizes actions in opts[:only]" do 856 | # when the action is in opts[:only] 857 | opts = [model: Post, only: :show] 858 | params = %{"id" => 1} 859 | conn = conn( 860 | %Plug.Conn{ 861 | private: %{phoenix_action: :show}, 862 | assigns: %{current_user: %User{id: 1}} 863 | }, 864 | :get, 865 | "/posts/1", 866 | params 867 | ) 868 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 869 | assert authorize_resource(conn, opts) == expected 870 | 871 | 872 | # when the action is not opts[:only] 873 | opts = [model: Post, only: :other] 874 | params = %{"id" => 1} 875 | conn = conn( 876 | %Plug.Conn{ 877 | private: %{phoenix_action: :show}, 878 | assigns: %{current_user: %User{id: 1}} 879 | }, 880 | :get, 881 | "/posts/1", 882 | params 883 | ) 884 | expected = conn 885 | 886 | assert authorize_resource(conn, opts) == expected 887 | end 888 | 889 | 890 | test "it only loads and authorizes the resource for actions in opts[:only]" do 891 | # when the action is in opts[:only] 892 | opts = [model: Post, only: :show] 893 | params = %{"id" => 1} 894 | conn = conn( 895 | %Plug.Conn{ 896 | private: %{phoenix_action: :show}, 897 | assigns: %{current_user: %User{id: 1}} 898 | }, 899 | :get, 900 | "/posts/1", 901 | params 902 | ) 903 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 904 | expected = %{conn | assigns: Map.put(expected.assigns, :post, %Post{id: 1, user_id: 1})} 905 | 906 | assert load_and_authorize_resource(conn, opts) == expected 907 | 908 | 909 | # when the action is not opts[:only] 910 | opts = [model: Post, only: :other] 911 | params = %{"id" => 1} 912 | conn = conn( 913 | %Plug.Conn{ 914 | private: %{phoenix_action: :show}, 915 | assigns: %{current_user: %User{id: 1}} 916 | }, 917 | :get, 918 | "/posts/1", 919 | params 920 | ) 921 | expected = conn 922 | 923 | assert load_and_authorize_resource(conn, opts) == expected 924 | end 925 | 926 | 927 | test "it skips the plug when both opts[:only] and opts[:except] are specified" do 928 | # when the plug is load_resource 929 | opts = [model: Post, only: :show, except: :index] 930 | params = %{"id" => 1} 931 | conn = conn( 932 | %Plug.Conn{ 933 | private: %{phoenix_action: :show}, 934 | assigns: %{current_user: %User{id: 1}} 935 | }, 936 | :get, 937 | "/posts/1", 938 | params 939 | ) 940 | expected = conn 941 | 942 | assert load_resource(conn, opts) == expected 943 | 944 | 945 | # when the plug is authorize_resource 946 | opts = [model: Post, only: :show, except: :index] 947 | params = %{"id" => 1} 948 | conn = conn( 949 | %Plug.Conn{ 950 | private: %{phoenix_action: :show}, 951 | assigns: %{current_user: %User{id: 1}} 952 | }, 953 | :get, 954 | "/posts/1", 955 | params 956 | ) 957 | expected = conn 958 | 959 | assert authorize_resource(conn, opts) == expected 960 | 961 | 962 | # when the plug is load_and_authorize_resource 963 | opts = [model: Post, only: :show, except: :index] 964 | params = %{"id" => 1} 965 | conn = conn( 966 | %Plug.Conn{ 967 | private: %{phoenix_action: :show}, 968 | assigns: %{current_user: %User{id: 1}} 969 | }, 970 | :get, 971 | "/posts/1", 972 | params 973 | ) 974 | expected = conn 975 | 976 | assert load_and_authorize_resource(conn, opts) == expected 977 | end 978 | 979 | test "it correctly skips authorization for exempt actions" do 980 | # when the action is exempt 981 | opts = [model: Post, except: :show] 982 | params = %{"id" => 1} 983 | conn = conn( 984 | %Plug.Conn{ 985 | private: %{phoenix_action: :show}, 986 | assigns: %{current_user: %User{id: 1}} 987 | }, 988 | :get, 989 | "/posts/1", 990 | params 991 | ) 992 | expected = conn 993 | 994 | assert authorize_resource(conn, opts) == expected 995 | 996 | 997 | # when the action is not exempt 998 | opts = [model: Post] 999 | expected = %{conn | assigns: Map.put(expected.assigns, :authorized, true)} 1000 | 1001 | assert authorize_resource(conn, opts) == expected 1002 | end 1003 | 1004 | 1005 | test "it correctly skips loading resources for exempt actions" do 1006 | # when the action is exempt 1007 | opts = [model: Post, except: :show] 1008 | params = %{"id" => 1} 1009 | conn = conn( 1010 | %Plug.Conn{ 1011 | private: %{phoenix_action: :show}, 1012 | assigns: %{current_user: %User{id: 1}} 1013 | }, 1014 | :get, 1015 | "/posts/1", 1016 | params 1017 | ) 1018 | expected = conn 1019 | assert load_resource(conn, opts) == expected 1020 | 1021 | 1022 | # when the action is not exempt 1023 | opts = [model: Post] 1024 | expected = %{conn | assigns: Map.put(expected.assigns, :post, %Post{id: 1, user_id: 1})} 1025 | assert load_resource(conn, opts) == expected 1026 | end 1027 | 1028 | 1029 | test "it correctly skips load_and_authorize_resource for exempt actions" do 1030 | # when the action is exempt 1031 | opts = [model: Post, except: :show] 1032 | params = %{"id" => 1} 1033 | conn = conn( 1034 | %Plug.Conn{ 1035 | private: %{phoenix_action: :show}, 1036 | assigns: %{current_user: %User{id: 1}} 1037 | }, 1038 | :get, 1039 | "/posts/1", 1040 | params 1041 | ) 1042 | expected = conn 1043 | assert load_and_authorize_resource(conn, opts) == expected 1044 | 1045 | 1046 | # when the action is not exempt 1047 | opts = [model: Post] 1048 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 1049 | expected = %{expected | assigns: Map.put(expected.assigns, :post, %Post{id: 1, user_id: 1})} 1050 | assert load_and_authorize_resource(conn, opts) == expected 1051 | end 1052 | 1053 | 1054 | test "it loads the resource into a key specified by the :as option" do 1055 | opts = [model: Post, as: :some_key] 1056 | 1057 | # when the resource with the id can be fetched 1058 | params = %{"id" => 1} 1059 | conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/1", params) 1060 | expected = %{conn | assigns: Map.put(conn.assigns, :some_key, %Post{id: 1})} 1061 | 1062 | assert load_resource(conn, opts) == expected 1063 | end 1064 | 1065 | 1066 | test "it authorizes the resource correctly when the :as key is specified" do 1067 | opts = [model: Post, as: :some_key] 1068 | 1069 | # when the action is "new" 1070 | params = %{} 1071 | conn = conn( 1072 | %Plug.Conn{ 1073 | private: %{phoenix_action: :new}, 1074 | assigns: %{current_user: %User{id: 1}} 1075 | }, 1076 | :get, 1077 | "/posts/new", 1078 | params 1079 | ) 1080 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 1081 | 1082 | assert authorize_resource(conn, opts) == expected 1083 | # need to check that it works for authorization as well, and for load_and_authorize_resource 1084 | end 1085 | 1086 | 1087 | test "it loads and authorizes the resource correctly when the :as key is specified" do 1088 | opts = [model: Post, as: :some_key] 1089 | 1090 | # when the current user can access the given resource 1091 | # and the resource can be loaded 1092 | params = %{"id" => 1} 1093 | conn = conn( 1094 | %Plug.Conn{ 1095 | private: %{phoenix_action: :show}, 1096 | assigns: %{current_user: %User{id: 1}} 1097 | }, 1098 | :get, 1099 | "/posts/1", 1100 | params 1101 | ) 1102 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 1103 | expected = %{expected | assigns: Map.put(expected.assigns, :some_key, %Post{id: 1, user_id: 1})} 1104 | 1105 | assert load_and_authorize_resource(conn, opts) == expected 1106 | end 1107 | 1108 | 1109 | test "when the :as key is not specified, it loads the resource into a key inferred from the model name" do 1110 | opts = [model: Post] 1111 | 1112 | # when the resource with the id can be fetched 1113 | params = %{"id" => 1} 1114 | conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/1", params) 1115 | expected = %{conn | assigns: Map.put(conn.assigns, :post, %Post{id: 1})} 1116 | 1117 | assert load_resource(conn, opts) == expected 1118 | end 1119 | 1120 | test "when unauthorized, it calls the specified action" do 1121 | opts = [model: Post, unauthorized_handler: {Helpers, :unauthorized_handler}] 1122 | 1123 | params = %{"id" => 1} 1124 | conn = conn(%Plug.Conn{assigns: %{current_user: %User{id: 2}}, 1125 | private: %{phoenix_action: :show}}, :get, "/posts/1", params) 1126 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 1127 | expected = Helpers.unauthorized_handler(expected) 1128 | 1129 | assert authorize_resource(conn, opts) == expected 1130 | end 1131 | 1132 | test "when not_found, it calls the specified action" do 1133 | opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}] 1134 | 1135 | params = %{"id" => 3} 1136 | conn = conn(%Plug.Conn{assigns: %{post: nil}, private: %{phoenix_action: :show}}, :get, "/posts/3", params) 1137 | 1138 | expected = Helpers.not_found_handler(conn) 1139 | 1140 | assert load_resource(conn, opts) == expected 1141 | end 1142 | 1143 | test "when unauthorized and resource not found, it calls the specified authorization handler first" do 1144 | opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}, 1145 | unauthorized_handler: {Helpers, :unauthorized_handler}] 1146 | 1147 | params = %{"id" => 3} 1148 | conn = conn(%Plug.Conn{assigns: %{current_user: %User{id: 2}}, 1149 | private: %{phoenix_action: :show}}, :get, "/posts/3", params) 1150 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 1151 | expected = %{expected | assigns: Map.put(expected.assigns, :post, nil)} 1152 | expected = Helpers.unauthorized_handler(expected) 1153 | 1154 | assert load_and_authorize_resource(conn, opts) == expected 1155 | end 1156 | 1157 | test "when the authorization handler does not halt the request, it calls the not found handler if specified" do 1158 | opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}, 1159 | unauthorized_handler: {Helpers, :non_halting_unauthorized_handler}] 1160 | 1161 | params = %{"id" => 3} 1162 | conn = conn(%Plug.Conn{assigns: %{current_user: %User{id: 2}}, 1163 | private: %{phoenix_action: :show}}, :get, "/posts/3", params) 1164 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 1165 | expected = %{expected | assigns: Map.put(expected.assigns, :post, nil)} 1166 | expected = expected 1167 | |> Helpers.non_halting_unauthorized_handler 1168 | |> Helpers.not_found_handler 1169 | 1170 | assert load_and_authorize_resource(conn, opts) == expected 1171 | end 1172 | 1173 | defmodule UnauthorizedHandlerConfigured do 1174 | use ExUnit.Case, async: false 1175 | 1176 | test "when unauthorized, it calls the configured action" do 1177 | Application.put_env(:canary, :unauthorized_handler, {Helpers, :unauthorized_handler}) 1178 | opts = [model: Post] 1179 | 1180 | params = %{"id" => 1} 1181 | conn = conn(%Plug.Conn{assigns: %{current_user: %User{id: 2}}, 1182 | private: %{phoenix_action: :show}}, :get, "/posts/1", params) 1183 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 1184 | expected = Helpers.unauthorized_handler(expected) 1185 | 1186 | assert authorize_resource(conn, opts) == expected 1187 | end 1188 | 1189 | test "when unauthorized and resource not found, it calls the configured authorization handler first" do 1190 | Application.put_env(:canary, :unauthorized_handler, {Helpers, :unauthorized_handler}) 1191 | opts = [model: Post] 1192 | 1193 | params = %{"id" => 3} 1194 | conn = conn(%Plug.Conn{assigns: %{current_user: %User{id: 2}}, 1195 | private: %{phoenix_action: :show}}, :get, "/posts/3", params) 1196 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, false)} 1197 | expected = %{expected | assigns: Map.put(expected.assigns, :post, nil)} 1198 | expected = Helpers.unauthorized_handler(expected) 1199 | 1200 | assert load_and_authorize_resource(conn, opts) == expected 1201 | end 1202 | end 1203 | 1204 | defmodule UnauthorizedHandlerConfiguredAndSpecified do 1205 | use ExUnit.Case, async: false 1206 | 1207 | test "when unauthorized, it calls the opt-specified action rather than the configured action" do 1208 | Application.put_env(:canary, :unauthorized_handler, {Helpers, :does_not_exist}) # should not be called 1209 | opts = [model: Post, unauthorized_handler: {Helpers, :unauthorized_handler}] 1210 | 1211 | params = %{"id" => 1} 1212 | conn = conn(%Plug.Conn{assigns: %{current_user: %User{id: 2}}, 1213 | private: %{phoenix_action: :show}}, :get, "/posts/1", params) 1214 | expected = Helpers.unauthorized_handler(conn) 1215 | expected = %{expected | assigns: Map.put(expected.assigns, :authorized, false)} 1216 | 1217 | assert authorize_resource(conn, opts) == expected 1218 | end 1219 | end 1220 | 1221 | defmodule NotFoundHandlerConfigured do 1222 | use ExUnit.Case, async: false 1223 | 1224 | test "when not_found, it calls the configured action" do 1225 | Application.put_env(:canary, :not_found_handler, {Helpers, :not_found_handler}) 1226 | opts = [model: Post] 1227 | 1228 | params = %{"id" => 4} 1229 | conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/4", params) 1230 | expected = Helpers.not_found_handler(conn) 1231 | expected = %{expected | assigns: Map.put(expected.assigns, :post, nil)} 1232 | 1233 | assert load_resource(conn, opts) == expected 1234 | end 1235 | end 1236 | 1237 | defmodule NotFoundHandlerConfiguredAndSpecified do 1238 | use ExUnit.Case, async: false 1239 | 1240 | test "when not_found, it calls the opt-specified action rather than the configured action" do 1241 | Application.put_env(:canary, :not_found_handler, {Helpers, :does_not_exist}) # should not be called 1242 | opts = [model: Post, not_found_handler: {Helpers, :not_found_handler}] 1243 | 1244 | params = %{"id" => 4} 1245 | conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/4", params) 1246 | expected = Helpers.not_found_handler(conn) 1247 | expected = %{expected | assigns: Map.put(expected.assigns, :post, nil)} 1248 | 1249 | assert load_resource(conn, opts) == expected 1250 | end 1251 | end 1252 | 1253 | 1254 | defmodule CurrentUser do 1255 | use ExUnit.Case, async: true 1256 | 1257 | defmodule ApplicationConfig do 1258 | use ExUnit.Case, async: false 1259 | import Mock 1260 | 1261 | test_with_mock "it uses the current_user name configured", Application, [:passthrough], [ 1262 | get_env: fn(_,_,_)-> :current_admin end 1263 | ] do 1264 | # when the user configured with opts 1265 | opts = [model: Post, except: :show] 1266 | params = %{"id" => 1} 1267 | conn = conn( 1268 | %Plug.Conn{ 1269 | private: %{phoenix_action: :show}, 1270 | assigns: %{current_admin: %User{id: 1}} 1271 | }, 1272 | :get, 1273 | "/posts/1", 1274 | params 1275 | ) 1276 | expected = conn 1277 | 1278 | assert authorize_resource(conn, opts) == expected 1279 | end 1280 | end 1281 | 1282 | test "it uses the current_user name in options" do 1283 | # when the user configured with opts 1284 | opts = [model: Post, current_user: :user] 1285 | params = %{"id" => 1} 1286 | conn = conn( 1287 | %Plug.Conn{ 1288 | private: %{phoenix_action: :show}, 1289 | assigns: %{user: %User{id: 1}, authorized: true} 1290 | }, 1291 | :get, 1292 | "/posts/1", 1293 | params 1294 | ) 1295 | expected = conn 1296 | 1297 | assert authorize_resource(conn, opts) == expected 1298 | end 1299 | 1300 | test "it throws an error when the wrong current_user name is used" do 1301 | # when the user configured with opts 1302 | opts = [model: Post, current_user: :configured_current_user] 1303 | params = %{"id" => 1} 1304 | conn = conn( 1305 | %Plug.Conn{ 1306 | private: %{phoenix_action: :show}, 1307 | assigns: %{user: %User{id: 1}, authorized: true} 1308 | }, 1309 | :get, 1310 | "/posts/1", 1311 | params 1312 | ) 1313 | 1314 | assert_raise KeyError, "key :configured_current_user not found in: %{authorized: true, user: %User{id: 1}}", fn-> 1315 | authorize_resource(conn, opts) 1316 | end 1317 | end 1318 | end 1319 | 1320 | defmodule Preload do 1321 | use ExUnit.Case, async: true 1322 | 1323 | test "it loads the resource correctly when the :preload key is specified" do 1324 | opts = [model: Post, preload: :user] 1325 | 1326 | # when the resource with the id can be fetched and the association exists 1327 | params = %{"id" => 2} 1328 | conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/1", params) 1329 | expected = %{conn | assigns: Map.put(conn.assigns, :post, %Post{id: 2, user_id: 2, user: %User{id: 2}})} 1330 | 1331 | assert load_resource(conn, opts) == expected 1332 | 1333 | 1334 | # when the resource with the id can be fetched and the association does not exist 1335 | params = %{"id" => 1} 1336 | conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/1", params) 1337 | expected = %{conn | assigns: Map.put(conn.assigns, :post, %Post{id: 1, user_id: 1})} 1338 | 1339 | assert load_resource(conn, opts) == expected 1340 | 1341 | 1342 | # when the resource with the id cannot be fetched 1343 | params = %{"id" => 3} 1344 | conn = conn(%Plug.Conn{private: %{phoenix_action: :show}}, :get, "/posts/3", params) 1345 | expected = %{conn | assigns: Map.put(conn.assigns, :post, nil)} 1346 | 1347 | assert load_resource(conn, opts) == expected 1348 | 1349 | 1350 | # when the action is "index" 1351 | params = %{} 1352 | conn = conn(%Plug.Conn{private: %{phoenix_action: :index}}, :get, "/posts", params) 1353 | expected = %{conn | assigns: Map.put(conn.assigns, :posts, [%Post{id: 1}, %Post{id: 2, user_id: 2, user: %User{id: 2}}])} 1354 | 1355 | assert load_resource(conn, opts) == expected 1356 | 1357 | 1358 | # when the action is "new" 1359 | params = %{} 1360 | conn = conn(%Plug.Conn{private: %{phoenix_action: :new}}, :get, "/posts/new", params) 1361 | expected = %{conn | assigns: Map.put(conn.assigns, :post, nil)} 1362 | 1363 | assert load_resource(conn, opts) == expected 1364 | end 1365 | 1366 | test "it authorizes the resource correctly when the :preload key is specified" do 1367 | opts = [model: Post, preload: :user] 1368 | 1369 | # when the action is "edit" 1370 | params = %{"id" => 2} 1371 | conn = conn( 1372 | %Plug.Conn{ 1373 | private: %{phoenix_action: :edit}, 1374 | assigns: %{current_user: %User{id: 2}} 1375 | }, 1376 | :get, 1377 | "/posts/edit/2", 1378 | params 1379 | ) 1380 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 1381 | 1382 | assert authorize_resource(conn, opts) == expected 1383 | 1384 | 1385 | # when the action is "index" 1386 | params = %{} 1387 | conn = conn( 1388 | %Plug.Conn{ 1389 | private: %{phoenix_action: :index}, 1390 | assigns: %{current_user: %User{id: 1}} 1391 | }, 1392 | :get, 1393 | "/posts", 1394 | params 1395 | ) 1396 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 1397 | 1398 | assert authorize_resource(conn, opts) == expected 1399 | end 1400 | 1401 | test "it loads and authorizes the resource correctly when the :preload key is specified" do 1402 | opts = [model: Post, preload: :user] 1403 | 1404 | # when the current user can access the given resource 1405 | # and the resource can be loaded and the association exists 1406 | params = %{"id" => 2} 1407 | conn = conn( 1408 | %Plug.Conn{ 1409 | private: %{phoenix_action: :show}, 1410 | assigns: %{current_user: %User{id: 2}} 1411 | }, 1412 | :get, 1413 | "/posts/2", 1414 | params 1415 | ) 1416 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 1417 | expected = %{expected | assigns: Map.put(expected.assigns, :post, %Post{id: 2, user_id: 2, user: %User{id: 2}})} 1418 | 1419 | assert load_and_authorize_resource(conn, opts) == expected 1420 | 1421 | 1422 | # when the current user can access the given resource 1423 | # and the resource can be loaded and the association does not exist 1424 | params = %{"id" => 1} 1425 | conn = conn( 1426 | %Plug.Conn{ 1427 | private: %{phoenix_action: :show}, 1428 | assigns: %{current_user: %User{id: 1}} 1429 | }, 1430 | :get, 1431 | "/posts/1", 1432 | params 1433 | ) 1434 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 1435 | expected = %{expected | assigns: Map.put(expected.assigns, :post, %Post{id: 1, user_id: 1})} 1436 | 1437 | assert load_and_authorize_resource(conn, opts) == expected 1438 | 1439 | # when the action is "edit" 1440 | params = %{"id" => 2} 1441 | conn = conn( 1442 | %Plug.Conn{ 1443 | private: %{phoenix_action: :edit}, 1444 | assigns: %{current_user: %User{id: 2}} 1445 | }, 1446 | :get, 1447 | "/posts/edit/2", 1448 | params 1449 | ) 1450 | expected = %{conn | assigns: Map.put(conn.assigns, :authorized, true)} 1451 | expected = %{expected | assigns: Map.put(expected.assigns, :post, %Post{id: 2, user_id: 2, user: %User{id: 2}})} 1452 | 1453 | assert load_and_authorize_resource(conn, opts) == expected 1454 | end 1455 | end 1456 | end 1457 | --------------------------------------------------------------------------------