# Authy – Simple Authorization
Authy is a tiny Elixir authorization library that imposes a simple module naming convention to express authorization.
It also supplies some handy macros to DRY up controller actions in Phoenix and other Plug-based apps.
It's inspired by the Ruby gem [Pundit](https://github.com/elabs/pundit), so if you're a fan of Pundit, you'll see where Authy is coming from.
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
1. Add `authy` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[{:authy, "~> 0.1.0"}]
end
```
2. If you are using Phoenix or another Plug-based app, add this configuration to `config.exs`:
```elixir
config :authy,
unauthorized_handler: {MyApp.AuthyCallbacks, :handle_unauthorized},
not_found_handler: {MyApp.AuthyCallbacks, :handle_not_found}
```
You'll have to define that handler module. See the Phoenix section below for more info.
Also add `import Authy.Controller` to the `controller` section of `web.ex` to make macros available.
## Policies
Authorization logic is contained in **policy modules** – one module per resource to be authorized.
To define a policy for a `Post`, create a module `Post.Policy` with the authorization logic defined in `can?(user, action, term)` methods:
```elixir
defmodule Post.Policy do
# Admin users are god
def can?(%User{role: :admin}, _action, _post), do: true
# Regular users can modify their own posts
def can?(%User{id: user_id, role: :user}, _action, %Post{user_id: post_user_id})
when user_id == post_user_id, do: true
# Other users (including guest user, nil) can only index and view posts
def can?(_user, :index, Post), do: true
def can?(_user, :show, _post), do: true
# Catch-all: deny everything else
def can?(_, _, _), do: false
end
```
To query it:
```elixir
owner = %User{id: 1, role: :user}
other = %User{id: 2, role: :user}
admin = %User{id: 3, role: :admin}
post = %Post{user_id: 1}
Authy.authorized?(admin, :edit, post) # => true
Authy.authorized?(owner, :edit, post) # => true
Authy.authorized?(other, :edit, post) # => false
Authy.authorized?(nil, :edit, post) # => false
Authy.authorized?(admin, :show, post) # => true
Authy.authorized?(owner, :show, post) # => true
Authy.authorized?(other, :show, post) # => true
Authy.authorized?(nil, :show, post) # => true
# Note this use of the Post module, not the struct;
# they both defer to Post.Policy
Authy.authorized?(nil, :index, Post) # => true
```
## Policy Scopes
Another idea borrowed from Pundit, **policy scopes** are a way to embed logic about what resources a particular user can see or otherwise access.
It's just another simple naming convention. Define `scope(user, action, opts)` functions in your policy module to utilize it.
```elixir
defmodule Post.Policy
# ...
# Admin sees all posts
def scope(%User{role: :admin}, :index, _opts),
do: Ecto.Query.from(Post)
# User sees their posts only
def scope(%User{role: :user, id: id}, :index, _opts),
do: Ecto.Query.where(Post, user_id: ^id)
# Guest sees published posts only
def scope(nil, :index, _opts),
do: Ecto.Query.where(Post, published: true)
end
```
And to call it:
```elixir
Authy.scoped(user1, :index) # => posts for user id 1
Authy.scoped(user2, :index) # => posts for user id 2
Authy.scoped(admin, :index) # => all posts
Authy.scoped(nil, :index) # => published posts
```
You can also pass `opts` keywords to `Authy.scoped/3`, which will be passed along untouched to the `scope/3` method in your policy module, in case some extra parameters are required to build the scope.
## Phoenix and Other Plug Apps
The `Authy.Controller` module has two macros designed to provide authorization in controller actions, `authorize/2` and `scope/2`. They have reasonable defaults which can be overridden for particular cases.
The macros assume the variable `Plug.Conn` struct `conn` exists, and use it to determine the current user, controller action, and so on.
```elixir
defmodule MyApp.PostController do
use MyApp.Web, :controller
alias MyApp.Post
# Authy.Controller has been imported in web.ex
def index(conn, _params) do
authorize Post do # <-- block is only executed if authorized
posts = scope(Post) # <-- posts in :index are scoped to the current user
conn |> render("index.html", posts: posts)
end
end
def show(conn, %{"id" => id}) do
post = scope(Post) |> Repo.get(id) # <-- scope can even be used for lookup
authorize post do # <-- authorize the :show action for this particular post
conn |> render("show.html", post: post)
end
end
end
```
When authorization fails, Authy will call your unauthorized handler, as defined in the `:unauthorized_handler` config. The handler takes a single argument, `conn`, and should return a `Plug.Conn` with the appropriate adjustments.
You can probably just copy and paste this to start:
```elixir
defmodule MyApp.AuthyCallbacks do
def handle_unauthorized(conn) do
conn
|> Plug.Conn.put_status(:unauthorized)
|> Phoenix.Controller.html(MyApp.ErrorView.render("401.html"))
|> Plug.Conn.halt
end
def handle_not_found(conn) do
conn
|> Plug.Conn.put_status(:not_found)
|> Phoenix.Controller.html(MyApp.ErrorView.render("404.html"))
|> Plug.Conn.halt
end
end
```
In the event that a resource is nil, you may choose to trigger either "unauthorized" (default) or "not found" behavior. This can be customized at the library level by setting the `nils` config option to either `:unauthorized` or `:not_found`. It can also be customized at the action level by passing the same option to the `authorize` macro.
### Additional Options
`policy` – Override the policy module
```elixir
Authy.authorized?(user, :show, post, policy: Admin.Policy)
Authy.scoped(user, Post, policy: Admin.Policy)
# Using Authy.Controller
authorize post, policy: Admin.policy, do: #...
scope(Post, policy: Admin.policy)
```
`action` – Override the action
```elixir
authorize post, action: :publish, do: #...
scope(Post, action: :publish)
```
`user` – Override the current user
```elixir
authorize post, user: other_user, do: #...
scope(Post, user: other_user)
```
`nils` – Override the behavior for nil resources
```elixir
authorize post, nils: :unauthorized, do: #...
authorize post, nils: :not_found
```
## Recommendations
Here are a few helpful tips and conventions to follow when laying out Authy in your app.
### File Naming and Location
Limit one policy module per file, and name the files like `[MODEL]_policy.ex`, for example `user_policy.ex` and `post_policy.ex`.
For plain Elixir apps, place policies in `lib/policies`. For Phoenix web apps, put them in `web/policies` instead.
### Member Versus Collection actions
For collection actions like `:index`, pass in the module name (an atom) as the resource to be authorized, since there is no instance of data to check against, e.g. `MyApp.User`.
For individual resource actions like `:show`, pass in the struct data itself, e.g. `%MyApp.User{}`.
For scopes, it doesn't matter if you pass in the module or the data - either will work.
### Suggestion: Policy Helpers
Consider creating a generic **policy helper** to collect authorization logic that is common to many different parts of your application. Reuse it by importing it into more specific policies.
```elixir
defmodule MyApp.PolicyHelper do
# common methods here
end
defmodule MyApp.Post.Policy do
import MyApp.PolicyHelper
# ...
end
```
### Suggestion: Controller Policies
What if you have a Phoenix controller that doesn't correspond to one particular resource? Or, maybe you just want to customize how that controllers' actions are locked down.
Try creating a policy for the controller itself. `MyApp.FooController.Policy` is completely acceptable.
## Not What You're Looking For?
Check out these other Elixir authorization libraries:
* [Canada](https://github.com/jarednorman/canada)
* [Canary](https://github.com/cpjk/canary)
## Ideas for Future Work
* Similar to policy scopes, add **policy changesets**, which will build a changeset based on a users' privileges
* ...?
* Profit!
## License
MIT