README.md

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