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.


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

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

  2. Ensure sentry is started before your application:

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

  3. Ensure your `User` model and `users` table has the following fields:
    - `:encrypted_password`
    - `email`
    - and a virtual `password` field.

  ```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
  ```

  4. 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
 For authentication please follow the following example, please refer to [Ueberatuh Readme](https://github.com/ueberauth/ueberauth) for more detail

 sentry provides the Sentry.Authenticator.attempt/1 method for authenticating users
 this uses the ueberauth_identity stratergy to collect useful information into
 the %Auth{} struct

 ```elixir
 # web/controllers/auth_controller

 defmodule MyApp.AuthController do
  use MyApp.Web, :controller
  import Sentry.Authenticator
  alias Ueberauth.Strategy.Helpers
  alias MyApp.User

  def request(conn, %{"provider" => "identity"} = _params) do
    render(conn, callback_url: Helpers.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: Helpers.request_path(conn))
    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
  ```

  And last but not least the view

  ```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 you would like to authorize a create post action based on a set of conditions
and it'll only authorize when those conditions are met.

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

defmodule MyApp.PostController do
  import 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)
  end
end
```

the `authorize_changeset/2` macro basically does this

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

so, you can use something like [Plug.ErrorHandler](http://hexdocs.pm/plug/Plug.ErrorHandler.html) to handle these errors on the router level like maybe send them to a 401.html page?

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

defmodule MyApp.PostPolicy do
  import 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
```

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 not 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`

## License

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