README.md

# 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
- JWT

## Installation
Add sentry to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [{:sentry, "~> 0.1"}]
end
```

Ensure sentry is started before your application:

```elixir
def application do
  [applications: [:sentry]]
end
```

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 Ueberauth and Sentry in `config/config.exs`
```elixir
# config/config.exs

# Ueberauth
config :ueberauth, Ueberauth,
  providers: [
    identity: {Ueberauth.Strategy.Identity, [
      callback_methods: ["POST"]
    ]}
  ]

# Sentry
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 leverages [ueberauth](https://github.com/ueberauth/ueberauth) for the authentication layer with an addition of bcrypt encryption when storing and authenticating user.

Sentry provides useful helpers for working with users on your system

- `Sentry.Authenticator.attempt/1`
- `Sentry.Authenticator.logout/1`
- `Sentry.Authenticator.encrypt_password/1`
- `Sentry.Authenticator.logged_in?/1`
- `Sentry.Authenticator.current_user/1`

here's an example use case

```elixir
# web/controllers/auth_controller

defmodule MyApp.AuthController do
  use MyApp.Web, :controller
  use Sentry, :authenticator

  alias MyApp.User

  def request(conn, %{"provider" => "identity"} = _params) do
    render(conn, callback_url: callback_url(conn),
      changeset: User.changeset(%User{}))
  end

  def request(conn, _params) do
    conn
    |> put_status(:not_found)
    |> render(MyApp.ErrorView, "404.html")
  end

  def callback(%{ assigns: %{ ueberauth_failure: fails } } = conn, _) do
    conn
    |> put_flash(:error, "Failed to authenticate.")
    |> redirect(to: "/")
  end

  def callback(conn, %{"provider" => "identity"}) do
    case attempt(conn) do
      {:ok, conn} ->
        conn
        |> put_flash(:info, "You've successfully logged in")
        |> redirect(to: "/")
      {:error, reason} ->
        conn
        |> put_flash(:error, reason)
        |> redirect(to: request_path(conn))
    end
  end

  def delete(conn, _params) do
    conn
    |> put_flash(:info, "You've been logged out!")
    |> logout()
    |> redirect(to: "/")
  end
end
```

```elixir
# web/router.ex

defmodule MyApp.Router do
  ...
  pipeline :auth do
    plug Ueberauth
  end

  scope "/auth", MyApp do
    pipe_through [:browser, :auth]

    get "/logout", AuthController, :delete
    get "/:provider", AuthController, :request
    get "/:provider/callback", AuthController, :callback
    post "/:provider/callback", AuthController, :callback
  end
end
```

```elixir
# web/view/layout_view.ex
defmodule MyApp.LayoutView do
  use MyApp.Web, :view
  use Sentry, :view # this adds the logged_in?/1 and current_user/1 helper to the view
end
```

```elixir
# web/templates/auth/request.html.eex

<%= form_for @changeset, @callback_url, fn f -> %>
  <%= if f.errors != [] do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below:</p>
      <ul>
        <%= for {attr, message} <- f.errors do %>
          <li><%= humanize(attr) %> <%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="form-group">
    <label>Email</label>
    <%= text_input f, :email, class: "form-control" %>
  </div>

  <div class="form-group">
    <label>Password</label>
    <%= password_input f, :password, class: "form-control" %>
  </div>

  <div class="form-group">
    <%= submit "Login", class: "btn btn-primary" %>
  </div>
<% end %>
```

## Authorization
For authorization, we have 3 macros for dealing with it
- `Sentry.Authorizer.authorize/2`
- `Sentry.Authorizer.authorize_changeset/2`
- `Sentry.Authorizer.authorize_changeset/3`

Let's say a `PostController.Create` action should only be authorized when a set of conditions returns true.

```elixir
# web/controllers/post_controller.ex

defmodule MyApp.PostController do
  use Sentry, :authorizer

  alias MyApp.Repo
  alias MyApp.Post

  def update(conn, %{"id" => id, "post" => post_params}) do
    changeset = Repo.get!(Post, id)
    |> Post.changeset(params)

    authorize_changeset(conn, changeset) # you may optionally override the function to be executed on the policy module by passing a third argument. Example: :create
  end
end
```

the `authorize_changeset/2` macro basically does this

```elixir
unless MyApp.PostPolicy.update(conn, changeset) do
  raise Sentry.NotAuthorizedError
end
```

```elixir
# web/policies/post_policy.ex

defmodule MyApp.PostPolicy do
  use Sentry, :authenticator

  def update(conn, changeset) do
    current_user = current_user(conn) # here we use Sentry.Authenticator.current_user helper to get the current user in the session

    changeset.params.post["user_id"] === current_user.id # Only authorize when the post belongs to the current user
  end
end
```

### Authorization without resource/changeset

If you are not working on a changeset/resource you may opt to use the `Sentry.Authorizer.authorize/2` instead, the second optional argument can be used to pass data to the policy action.

Do take note the `Sentry.Authorizer.authorize/2` will use the policy name based on the controller name.

For example: an action on `UserController.create` will use `UserPolicy.create`

```elixir
# web/controllers/user_controller.ex

defmodule UserController do
  use Sentry, :authorizer

  def create(conn, params) do
    authorize(conn, params)
  end
end
```

```elixir
# web/policies/user_policy.ex

defmodule UserPolicy do
  def create(conn, opts) do
    true
  end
end
```

## Error Handling
To handle the errors raised by `Sentry.Authorizer.authorize`, we can use something like [Plug.ErrorHandler](http://hexdocs.pm/plug/Plug.ErrorHandler.html). This can either be plugged at the router level or controller.

## License

Sentry is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT)