# Sentry
"Sentry provides a set of helpers and conventions that will guide you in leveraging Elixir modules to build a simple, robust authorization system." - Inspired by [elabs/pundit](https://github.com/elabs/pundit)
## TODOs
- Generators
## Installation
Add sentry to your list of dependencies in `mix.exs`:
```elixir
def deps do
[{:sentry, "~> 0.3"}]
end
```
For authentication, ensure your `User` model and `users` table has the following fields:
- `:encrypted_password` field
- a user identification field. Defaults to `email`
- and a virtual password field. Defaults to `password`
```elixir
# web/models/user.ex
defmodule MyApp.User do
use MyApp.Web, :model
schema "users" do
field :email, :string
field :encrypted_password, :string
field :password, :string, virtual: true
field :password_confirmation, :string, virtual: true
...
timestamps
end
end
```
Configure Sentry
```elixir
# config/config.exs
config :sentry, Sentry,
repo: MyApp.Repo,
model: MyApp.User # you may use a different model as you like
# uid_field: :some_id_field \\ defaults to :email
# password_field: :some_pw_field \\ defaults to :password
```
## Authentication
Sentry provides useful helpers for working with users on your system
- `Authenticator.encrypt_password/1`
- `Authenticator.attempt/1`
### Authenticator.encrypt_password/1
Is used to encrypt the password field and add it to the changeset as 'encrypted_password'. Here's an example of a user creation
```elixir
def create_user(conn, %{"user" => user_params}) do
changeset = User.changeset(%User{}, user_params)
|> Authenticator.encrypt_password
case Repo.insert(changeset) do
{:ok, new_user} ->
conn
|> put_flash(:info, "You've successfully registered")
|> Guardian.Plug.sign_in(new_user, :token)
|> redirect(to: "/")
{:error, changeset} ->
render(conn, "register.html", changeset: changeset)
end
end
```
### Authenticator.attempt/1
Is used to attempt an authentication on a resource as specified in `config.exs`. In this example we used [guardian](https://github.com/hassox/guardian) to store the resource session using JWT. You can also just use `put_session`.
```elixir
# web/controllers/session_controller.ex
# Authenticator accepts the user params and tries to authenticate
# returning {:ok, authenticated_user} or {:error, changeset}
# you can then use the changeset to show authentication errors
def log_user_in(conn, %{"user" => user_params}) do
case Sentry.Authenticator.attempt(user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "You've successfully logged in")
|> Guardian.Plug.sign_in(user, :token)
|> redirect(to: "/")
{:error, changeset} ->
conn
|> render("login.html", changeset: changeset)
end
end
```
## Authorization
For authorization, we have the following functions for dealing with it
- `Authorizer.authorize/1` only on Phoenix
- `Authorizer.authorize/2` only on Phoenix
- `Authorizer.authorize/3`
Let's say that we have an `index` action in `page_controller.ex` that we only allow users who are logged in to be able to access.
There's a few way to do this. One is just as a normal `authorize/1` function
```elixir
# web/controllers/page_controller.ex
defmodule MyApp.PageController do
use Sentry, :authorizer # Make sure this line is included
def index(conn, _params) do
# you can optionally pass a second argument
# to be used in the policy example: authorize(conn, params)
case authorize(conn) do
{:ok, conn} ->
render(conn, "index.html")
{:error, reason} ->
conn
|> put_flash(:error, reason)
|> redirect(to: "/")
end
end
end
```
Or you can use it as a plug function in Phoenix controllers
```elixir
# web/controllers/page_controller.ex
defmodule MyApp.PageController do
...
use Sentry, :authorizer # Make sure this line is included
plug :authorize_action when action in [:index]
def index(conn, _params) do
...
end
def authorize_action(conn, _options) do
# you can optionally pass a second argument
# to be used in the policy example: authorize(conn, options)
case authorize(conn) do
{:ok, conn} ->
conn
{:error, reason} ->
conn
|> put_flash(:error, reason)
|> redirect(to: "/")
end
end
end
```
This will invoke a policy action based on the module name and action name, in the above example `authorize/1` will invoke the `SessionPolicy.index` which must return a tuple of `{:ok, conn} | {:error, reason}`
Let's write a policy for the `PageController.index/2` action
```elixir
# web/policies/page_policy.ex
defmodule MyApp.PagePolicy do
# the `option` argument is supplied if we use `authorize/2`
# if not it will be `nil`
def index(conn, _option) do
# Let's return {:ok, conn} if the user is logged in
# Otherwise return {:error, reason} if user is not logged in
# Let's assume that we have a `:current_user` stored in the session
# if the user is logged in
if !!get_session(conn, :current_user) do
{:ok, conn}
else
{:error, "You're already logged in"}
end
end
end
```
### Authorizing resource/changeset
If you are working on resource/changeset, sentry is clever enough to use a policy named after the resource instead of the module it is authorizing, the function name however will use the action it is authorizing. Do take note that the function name is overridable if we pass a third argument.
Example:
```elixir
def update(conn, %{"id" => id, "post" => post_params}) do
...
changeset = Post.changeset(post, post_params)
# you can pass an optional third argument as an
# atom to override the function
# to be executed on the policy for example:
# authorize(conn, changeset, :belongs_to_current_user)
# this will instead run the
# `PostPolicy.belongs_to_current_user/2` action
authorize(conn, changeset)
...
end
```
Which in turn will use a policy named after the model. In this case the `Post` model will use the `PostPolicy` policy
```elixir
# web/policies/post_policy.ex
defmodule PostPolicy do
use Sentry, :authenticator
def update(conn, changeset) do
...
end
end
```
## Headless policy
Sometimes you just want to authorize a couple of actions using the same policy again and again. In this case using a headless policy and a plug module might be more suitable.
We can authorize the same policy by passing the policy module and action in the second and third argument.
Also headless policy works without phoenix
Let's create a plug to demonstrate
```elixir
# web/plugs/ensure_authenticated.ex
defmodule MyApp.EnsureAuthenticated do
@behaviour Plug
import Sentry.Authorizer, only: [authorize: 3] # we don't use `use` in this case.
import Phoenix.Controller
def init(opts) do
opts
end
def call(conn, opts) do
# authorize(conn, policy, function_name: [arguments])
case authorize(conn, MyApp.SessionPolicy, authenticated: opts) do
{:ok, conn} ->
conn
{:error, reason} ->
conn
|> put_flash(:error, reason)
|> redirect(to: "/login")
end
end
end
```
and the policy for the above plug
```elixir
# web/policies/session_policy.ex
defmodule MyApp.SessionPolicy do
def authenticated(conn, opts) do
if !!current_resource(conn) do
{:ok, conn}
else
{:error, "You're not signed in"}
end
end
end
```
Now we can use the plug in multiple places. Let's rewrite our page controller to use this plug
```elixir
# web/controller/page_controller.ex
defmodule MyApp.PageController do
...
plug MyApp.EnsureAuthenticated
def index(conn, _params) do
render(conn, "index.html")
end
end
```
## License
Sentry is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT)