Skip to content

Commit 61cee16

Browse files
authored
Merge pull request #88 from cpjk/liveview
LiveView authentication - Canary 2.0.0
2 parents 49b47bf + bd526f1 commit 61cee16

22 files changed

+2554
-375
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ erl_crash.dump
44
*.ez
55
tags
66
/doc
7+
/cover
78
*.beam
9+
.history
10+
.tool-version

CHANGELOG.md

+15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
## Changelog
22

3+
## v2.0.0-dev
4+
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.
5+
6+
* Enhancements
7+
* added support for authorization LiveView with `Canary.Hooks`
8+
* added `:error_handler` and ErrorHandler behaviour
9+
* added `:required` option, default to true
10+
11+
* Dependency changes
12+
* Elixir ~> 1.14 is now required
13+
14+
* Deprecations
15+
* 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.
16+
* The `:persisted` option is deprecated and will be removed in Canary 2.1.0. Use `:required` instead.
17+
318
## v1.2.0
419
* Enhancements
520
* Add `required` opt

README.md

+147-50
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,15 @@ Canary
33
[![Actions Status](https://github.com/cpjk/canary/workflows/CI/badge.svg)](https://github.com/runhyve/canary/actions?query=workflow%3ACI)
44
[![Hex pm](https://img.shields.io/hexpm/v/canary.svg?style=flat)](https://hex.pm/packages/canary)
55

6-
An authorization library in Elixir for Plug applications that restricts what resources
7-
the current user is allowed to access, and automatically loads resources for the current request.
6+
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.
87

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

1110
[Read the docs](http://hexdocs.pm/canary)
1211

1312
## Installation
1413

15-
For the latest master:
14+
For the latest master (2.0.0-dev):
1615

1716
```elixir
1817
defp deps do
@@ -24,59 +23,75 @@ For the latest release:
2423

2524
```elixir
2625
defp deps do
27-
{:canary, "~> 1.2.1"}
26+
{:canary, "~> 2.0.0-dev"}
2827
end
2928
```
3029

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

33-
## Usage
32+
## Quick start
3433

35-
Canary provides three functions to be used as plugs to load and authorize resources:
34+
Canary provides functions to be used as plugs or LiveView hooks to load and authorize resources:
3635

37-
`load_resource/2`, `authorize_resource/2`, and `load_and_authorize_resource/2`.
36+
`load_resource`, `authorize_resource`, `authorize_controller`*, and `load_and_authorize_resource`.
3837

39-
`load_resource/2` and `authorize_resource/2` can be used by themselves, while `load_and_authorize_resource/2` combines them both.
38+
`load_resource` and `authorize_resource` can be used by themselves, while `load_and_authorize_resource` combines them both.
39+
40+
*Available only in plug based authentication*
4041

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

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

45-
- An Ecto record struct containing the user to authorize in `conn.assigns.current_user` (the key can be customized - see https://github.com/cpjk/canary#overriding-the-default-user).
46+
- An Ecto record struct containing the user to authorize in `assigns.current_user` (the key can be customized - [see more](#overriding-the-default-user)).
4647

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

49-
Then, just `import Canary.Plugs` in order to use the 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.
50+
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.
5051

51-
### load_resource/2
52+
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.
5253

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

55-
For example,
55+
### load_resource
5656

57+
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.
58+
59+
<!-- tabs-open -->
60+
### Conn Plugs example
5761
```elixir
5862
plug :load_resource, model: Project.Post
5963
```
60-
Will load the `Project.Post` having the id given in `conn.params["id"]` through `YourApp.Repo`, into
61-
`conn.assigns.post`
6264

63-
### authorize_resource/2
65+
Will load the `Project.Post` having the id given in `conn.params["id"]` through `YourApp.Repo`, and assign it to `conn.assigns.post`.
66+
67+
### LiveView Hooks example
68+
```elixir
69+
mount_canary :load_resource, model: Project.Post
70+
```
71+
72+
Will load the `Project.Post` having the id given in `params["id"]` through `YourApp.Repo`, and assign it to `socket.assigns.post`
73+
<!-- tabs-close -->
6474

65-
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 `conn.assigns.authorized`. It is up to you to decide what to do with the result.
75+
### authorize_resource
76+
77+
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.
6678

6779
For Phoenix applications, Canary determines the action automatically.
80+
For non-Phoenix applications, or to override the action provided by Phoenix, simply ensure that `assigns.canary_action` contains an atom specifying the action.
81+
82+
For the LiveView on `handle_params` it uses `socket.assigns.live_action` as action, on `handle_event` it uses the event name as action.
83+
6884

69-
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.
7085

7186
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).
7287

73-
### load_and_authorize_resource/2
88+
### load_and_authorize_resource
7489

75-
Authorizes the resource and then loads it if authorization succeeds. Again, the resource is loaded into `conn.assigns.<resource_name>`.
90+
Authorizes the resource and then loads it if authorization succeeds. Again, the resource is loaded into `assigns.<resource_name>`.
7691

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

79-
### Usage Example
94+
## Usage Example
8095

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

@@ -90,7 +105,10 @@ defimpl Canada.Can, for: User do
90105
def can?(%User{ id: user_id }, _, _), do: false
91106
end
92107
```
93-
and in your `web/router.ex:` you have:
108+
109+
### Example for Conn Plugs
110+
111+
In your `web/router.ex:` you have:
94112

95113
```elixir
96114
get "/posts/:id", PostController, :show
@@ -107,6 +125,46 @@ In this case, on `GET /posts/12` authorization succeeds, and the `Post` specifie
107125

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

128+
### Example for LiveView Hooks
129+
130+
In your `web/router.ex:` you have:
131+
132+
```elixir
133+
live "/posts/:id", PostLive, :show
134+
```
135+
136+
and in your PostLive module `web/live/post_live.ex`:
137+
138+
```elixir
139+
defmodule MyAppWeb.PostLive do
140+
use MyAppWeb, :live_view
141+
142+
def render(assigns) do
143+
~H"""
144+
Post id: {@post.id}
145+
<button phx-click="delete">Delete</button>
146+
"""
147+
end
148+
149+
def mount(_params, _session, socket), do: {:ok, socket}
150+
151+
def handle_event("delete", _params, socket) do
152+
# Do the action
153+
{:noreply, update(socket, :temperature, &(&1 + 1))}
154+
end
155+
end
156+
```
157+
158+
To automatically load and authorize on the `Post` having the `id` given in the params, you would add the following hook to your `PostLive`:
159+
160+
```elixir
161+
mount_hook :load_and_authorize_resource, model: Post
162+
```
163+
164+
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`.
165+
166+
However, when the `delete` event will be triggered, authorization fails and the `Post` resource is not loaded. Socket will be halted.
167+
110168
### Excluding actions
111169

112170
To exclude an action from any of the plugs, pass the `:except` key, with a single action or list of actions.
@@ -117,12 +175,16 @@ Single action form:
117175

118176
```elixir
119177
plug :load_and_authorize_resource, model: Post, except: :show
178+
179+
mount_canary :load_and_authorize_resource, model: Post, except: :show
120180
```
121181

122182
List form:
123183

124184
```elixir
125185
plug :load_and_authorize_resource, model: Post, except: [:show, :create]
186+
187+
mount_canary :load_and_authorize_resource, model: Post, except: [:show, :create]
126188
```
127189

128190
### Authorizing only specific actions
@@ -135,15 +197,19 @@ Single action form:
135197

136198
```elixir
137199
plug :load_and_authorize_resource, model: Post, only: :show
200+
201+
mount_canary :load_and_authorize_resource, model: Post, only: :show
138202
```
139203

140204
List form:
141205

142206
```elixir
143207
plug :load_and_authorize_resource, model: Post, only: [:show, :create]
208+
209+
mount_canary :load_and_authorize_resource, model: Post, only: [:show, :create]
144210
```
145211

146-
Note: Passing both `:only` and `:except` to a plug is invalid. Canary will simply pass the `Conn` along unchanged.
212+
> Note: Having both `:only` and `:except` in opts is invalid. Canary will raise `ArgumentError` "You can't use both :except and :only options"
147213
148214
### Overriding the default user
149215

@@ -153,12 +219,14 @@ Globally, the default key for finding the user to authorize can be set in your c
153219
config :canary, current_user: :some_current_user
154220
```
155221

156-
In this case, canary will look for the current user record in `conn.assigns.some_current_user`.
222+
In this case, canary will look for the current user record in `assigns.some_current_user`.
157223

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

160226
```elixir
161227
plug :load_and_authorize_resource, model: Post, current_user: :current_admin
228+
229+
mount_canary :load_and_authorize_resource, model: Post, current_user: :current_admin
162230
```
163231

164232
### Specifying resource_name
@@ -169,26 +237,35 @@ For example,
169237

170238
```elixir
171239
plug :load_and_authorize_resource, model: Post, as: :new_post
240+
241+
mount_canary :load_and_authorize_resource, model: Post, as: :new_post
172242
```
173243

174-
will load the post into `conn.assigns.new_post`
244+
will load the post into `assigns.new_post`
175245

176246
### Preloading associations
177247

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

180250
```elixir
181251
plug :load_and_authorize_resource, model: Post, preload: :comments
252+
253+
mount_canary :load_and_authorize_resource, model: Post, preload: :comments
182254
```
183255

184256
### Non-id actions
185257

186-
For the `:index`, `:new`, and `:create` actions, the resource passed to the `Canada.Can` implementation
187-
should be the *module* name of the model rather than a struct.
258+
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.
188259

189-
For example, when authorizing access to the `Post` resource,
260+
To authorize such actions use `authorize_resource` plug with `required: false` option
190261

191-
you should use
262+
```elixir
263+
plug :authorize_resource, model: Post, only: [:index, :new, :create], required: false
264+
265+
mount_canary :authorize_resource, model: Post, only: [:index, :new, :create], required: false
266+
```
267+
268+
For example, when authorizing access to the `Post` resource, you should use
192269

193270
```elixir
194271
def can?(%User{}, :index, Post), do: true
@@ -200,13 +277,51 @@ instead of
200277
def can?(%User{}, :index, %Post{}), do: true
201278
```
202279

203-
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.
280+
> ### Deprecated {: .warning}
281+
>
282+
> The `:non_id_actions` is deprecated as of 2.0.0-dev and will be removed in Canary 2.1.0
283+
> 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.
284+
285+
### Nested associations
286+
287+
Sometimes you need to load and authorize a parent resource when you have
288+
a relationship between two resources and you are creating a new one or
289+
listing all the children of that parent. Depending on your authorization
290+
model you migth authorize against the parent resource or against the child.
204291

205-
For example,
206292
```elixir
207-
plug :authorize_resource, model: Post, non_id_actions: [:find_by_name]
293+
defmodule MyAppWeb.CommentController do
294+
295+
plug :load_and_authorize_resource,
296+
model: Post,
297+
id_name: "post_id",
298+
only: [:new_comment, :create_comment]
299+
300+
# get /posts/:post_id/comments/new
301+
def new_comment(conn, _params) do
302+
# ...
303+
end
304+
305+
# post /posts/:post_id/comments
306+
def new_comment(conn, _params) do
307+
# ...
308+
end
309+
end
208310
```
209311

312+
It will authorize using `Canada.Can` with following arguments:
313+
1. subject is `conn.assigns.current_user`
314+
2. action is `:new_comment` or `:create_comment`
315+
3. resource is `%Post{}` with `conn.params["post_id"]`
316+
317+
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.
318+
If for some reason you want to disable it, set `required: false` in opts.
319+
320+
> ### Deprecated {: .warning}
321+
>
322+
> The `:persisted` is deprecated as of 2.0.0-dev and will be removed in Canary 2.1.0
323+
> 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.
324+
210325
### Implementing Canada.Can for an anonymous user
211326

212327
You may wish to define permissions for when there is no logged in current user (when `conn.assigns.current_user` is `nil`).
@@ -220,24 +335,6 @@ defimpl Canada.Can, for: Atom do
220335
end
221336
```
222337

223-
### Nested associations
224-
225-
Sometimes you need to load and authorize a parent resource when you have a relationship between two resources and you are
226-
creating a new one or listing all the children of that parent. By specifying the `:persisted` option with `true`
227-
you can load and/or authorize a nested resource. Specifying this option overrides the default loading behavior of the
228-
`:index`, `:new`, and `:create` actions by loading an individual resource. It also overrides the default
229-
authorization behavior of the `:index`, `:new`, and `create` actions by loading a struct instead of a module
230-
name for the call to `Canada.can?`.
231-
232-
For example, when loading and authorizing a `Post` resource which can have one or more `Comment` resources, use
233-
234-
```elixir
235-
plug :load_and_authorize_resource, model: Post, id_name: "post_id", persisted: true, only: [:create]
236-
```
237-
238-
to load and authorize the parent `Post` resource using the `post_id` in /posts/:post_id/comments before you
239-
create the `Comment` resource using its parent.
240-
241338
### Specifing database field
242339

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

0 commit comments

Comments
 (0)