# Speakeasy
Middleware based authentication and authorization for [Absinthe](https://hexdocs.pm/absinthe) GraphQL powered by [Bodyguard](https://hexdocs.pm/bodyguard)
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `speakeasy` to your list of dependencies in `mix.exs`:
```elixir
def deps do
  [
    {:speakeasy, "~> 0.2.1"}
  ]
end
```
## Usage
There are two ways to use Speakeasy to authorize GraphQL queries and mutations.
Policies are just regular [Bodyguard](https://github.com/schrockwell/bodyguard) policies with two small changes:
1.  Your `authorize/3` functions will receive the GraphQL `context` instead of a `user`. (Your context, probably includes the user).
2.  Your policies are written for GraphQL queries and mutations rather than bounded contexts.
```elixir
defmodule MyAppWeb.Schema do
  use Absinthe.Schema
  def authorize(:create_post, %{current_user: user} = gql_context, post) do
    IO.inspect(user)
    IO.inspect(context)
    # Return :ok or true to permit
    # Return :error, {:error, reason}, or false to deny
  end
end
```
### Using Absinthe Middleware
Absinthe supports a middleware stack that can be modified at the field or schema level.
Below is an example of adding authentication and authorization to a GraphQL field.
```elixir
defmodule MyAppWeb.Schema do
  use Absinthe.Schema
  def authorize(:create_post, gql_context, post) do
    # Return :ok or true to permit
    # Return :error, {:error, reason}, or false to deny
  end
  mutation do
    @desc "Create a post"
    field :create_post, type: :post do
      middleware(Speakeasy.Authentication)
      # Optionally you can pass an atom as the second argument to
      # set the name of the key to use for checking the current user. The default is `:current_user`
      # middleware(Speakeasy.Authentication, user_key: :current_user)
      middleware(Speakeasy.Authorization)
      arg(:title, non_null(:string))
      arg(:body, non_null(:string))
      resolve(fn _, args, %{context: %{current_user: user}} ->
        MyApp.Posts.create_post(args, user)
      end)
    end
  end
end
```
Alternatively you can use `defdelegate` to separate your schema and policy code:
```elixir
defmodule MyAppWeb.Schema.Policy do
  def authorize(:create_post, gql_context, post) do
    # Return :ok or true to permit
    # Return :error, {:error, reason}, or false to deny
  end
end
defmodule MyAppWeb.Schema do
  use Absinthe.Schema
  defdelegate authorize(action, user, params), to: MyAppWeb.Schema.Policy
  mutation do
    @desc "Create a post"
    field :create_post, type: :post do
      middleware(Speakeasy.Authentication)
      middleware(Speakeasy.Authorization)
      arg(:title, non_null(:string))
      arg(:body, non_null(:string))
      resolve(fn _, args, %{context: %{current_user: user}} ->
        MyApp.Posts.create_post(args, user)
      end)
    end
  end
end
```
Check out the [documentation](https://hexdocs.pm/absinthe/Absinthe.Middleware.html) for more details on how to use Absinthe middleware.
### `Speakeasy.resolve/2` or `Speakeasy.resolve!/2`
If you don't like the idea of defining your policies at the schema level, you can use `Speakeasy.resolve/2` or `Speakeasy.resolve!/2` in line with your field's resolve function and define policies on your bounded contexts instead.
```elixir
defmodule MyApp.Posts do
  def authorize(:create_post, graphql_context, args) do
    # Return :ok or true to permit
    # Return :error, {:error, reason}, or false to deny
  end
  def create_post(post, user) do
    # Your logic here.
  end
end
defmodule MyAppWeb.Schema do
  use Absinthe.Schema
  mutation do
    @desc "Create a post"
    field :create_post, type: :post do
      # If the arity of `:create_post` is 2, it will receive the `post` arguments and the graphql `context`
      resolve(Speakeasy.resolve(MyApp.Posts, :create_post))
      # If you want to receive the `user` instead, pass `user_key: :the_key_you_stored_your_user_under`
      # resolve(Speakeasy.resolve(MyApp.Posts, :create_post, user_key: :current_user))
      # A convience atom is accepted `:user` that will default to returning the value of the context's `:current_user`
      # resolve(Speakeasy.resolve(MyApp.Posts, :create_post, :user))
      # Alternatively `resolve!/2` can be used for compile time checking that your resolution function supports the correct arity. It also accepts `:user_key`
      # resolve(Speakeasy.resolve!(MyApp.Posts, :create_post))
    end
  end
end
```
If authorized `resolve/2` and `resolve!/2` will return an anonymous function to Absinthe's `resolve` function wrapping your resolution function (`MyApp.Posts.create_post` above).
Speakeasy will provide different arguments depending on your resolution functions arity. For example:
- `MyApp.Posts.list_post/0` - speakeasy will simply call this function
- `MyApp.Posts.create_post/1` - speakeasy will call this function passing the GraphQL arguments
- `MyApp.Posts.create_post/2` - speakeasy will call this function passing the GraphQL arguments as the first parameter and the GraphQL `context` _or_ `user` as the second depending on if `user_key` was provided.