README.md

# 🛡 Terminator with uuid 🛡

[![Version](https://img.shields.io/hexpm/v/terminator_uuid.svg?style=flat-square)](https://hex.pm/packages/terminator_uuid)

Terminator is toolkit for granular ability management for performers. This differs from terminator in now the id key associations use ecto uuid vs integers It allows you to define granular abilities such as:


- `Performer -> Ability`
- `Performer -> [Ability, Ability, ...]`
- `Role -> [Ability, Ability, ...]`
- `Performer -> Role -> [Ability, Ability, Ability]`
- `Performer -> [Role -> [Ability], Role -> [Ability, ...]]`
- `Performer -> AnyEntity -> [Ability, ...]`

It tries to mimic [https://en.wikipedia.org/wiki/Attribute-based_access_control](https://en.wikipedia.org/wiki/Attribute-based_access_control) and allow to define any policy which is needed.

Here is a small example:

```elixir
defmodule Sample.Post
  use Terminator

  def delete_post(uuid, performer_uuid \\ Ecto.UUID.bingenerate()) do
    performer = Terminator.Repo.get(Terminator.UUID.Performer, performer_uuid)
    load_and_authorize_performer(performer)

    post = %Post{id: uuid}

    permissions do
      has_role(:admin) # or
      has_role(:editor) # or
      has_ability(:delete_posts) # or
      has_ability(:delete, post) # Entity related abilities
      calculated(fn performer ->
        performer.email_confirmed?
      end)
    end

    as_authorized do
      Sample.Repo.get(Sample.Post, uuid) |> Sample.repo.delete()
    end

    # Notice that you can use both macros or functions

    case is_authorized? do
      :ok -> Sample.Repo.get(Sample.Post, uuid) |> Sample.repo.delete()
      {:error, message} -> "Raise error"
      _ -> "Raise error"
    end
  end

```

## Features

- [x] `Performer` -> `[Ability]` permission schema
- [x] `Role` -> `[Ability]` permission schema
- [x] `Performer` -> `[Role]` -> `[Ability]` permission schema
- [x] `Performer` -> `Object` -> `[Ability]` permission schema
- [x] Computed permission in runtime
- [x] Easily readable DSL
- [ ] [ueberauth](https://github.com/ueberauth/ueberauth) integration
- [ ] [absinthe](https://github.com/absinthe-graphql/absinthe) middleware
- [ ] Session plug to get current_user

## Installation

```elixir
def deps do
  [
    {:terminator_uuid, "~> 0.5.2"}
  ]
end
```

```elixir
# In your config/config.exs file
config :terminator_uuid, Terminator.UUID.Repo,
  username: "postgres",
  password: "postgres",
  database: "terminator_uuid_dev",
  hostname: "localhost"
```

```elixir
iex> mix terminator.UUID.setup
```

### Usage with ecto

Terminator is originally designed to be used with Ecto. Usually you will want to have your own table for `Accounts`/`Users` living in your application. To do so you can link performer with `belongs_to` association within your schema.

```elixir
# In your migrations add performer_id field
defmodule Sample.Migrations.CreateUsersTable do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :username, :string
      add :performer_id, references(Terminator.UUID.Performer.table(), type: :binary_id)

      timestamps()
    end

    create unique_index(:users, [:username])
  end
end

```

This will allow you link any internal entity with 1-1 association to performers. Please note that you need to create performer on each user creation (e.g with `Terminator.UUID.Performer.changeset/2`) and call `put_assoc` inside your changeset

```elixir
# In schema defintion
defmodule Sample.User do
  use Ecto.Schema

  schema "users" do
    field :username, :String

    belongs_to :performer, Terminator.UUID.Performer

    timestamps()
  end
end
```

```elixir
# In your model
defmodule Sample.Post
  use Terminator

  def delete_post(uuid, performer_uuid) do
    user = Sample.Repo.get(Sample.User, performer_uuid)
    load_and_authorize_performer(user)
    # Function allows multiple signatues of performer it can
    # be either:
    #  * %Terminator.UUID.Performer{}
    #  * %AnyStruct{performer: %Terminator.UUID.Performer{}}
    #  * %AnyStruct{performer_id: id} (this will perform database preload)


    permissions do
      has_role(:admin) # or
      has_role(:editor) # or
      has_ability(:delete_posts) # or
    end

    as_authorized do
      Sample.Repo.get(Sample.Post, uuid) |> Sample.repo.delete()
    end

    # Notice that you can use both macros or functions

    case is_authorized? do
      :ok -> Sample.Repo.get(Sample.Post, uuid) |> Sample.repo.delete()
      {:error, message} -> "Raise error"
      _ -> "Raise error"
    end
  end

```

Terminator tries to infer the performer, so it is easy to pass any struct (could be for example `User` in your application) which has set up `belongs_to` association for performer. If the performer was already preloaded from database Terminator will take it as loaded performer. If you didn't do preload and just loaded `User` -> `Repo.get(User, 1)` Terminator will fetch the performer on each authorization try.

### Calculated permissions

Often you will come to case when `static` permissions are not enough. For example allow only users who confirmed their email address.

```elixir
defmodule Sample.Post do
  def create(uuid, performer_uuid) do
    user = Sample.Repo.get(Sample.User, performer_uuid)
    load_and_authorize_performer(user)

    permissions do
      calculated(fn performer -> do
        performer.email_confirmed?
      end)
    end
  end
end
```

We can also use DSL form of `calculated` keyword

```elixir
defmodule Sample.Post do
  def create(uuid, performer_uuid) do
    user = Sample.Repo.get(Sample.User, performer_uuid)
    load_and_authorize_performer(user)

    permissions do
      calculated(:confirmed_email)
    end
  end

  def confirmed_email(performer) do
    performer.email_confirmed?
  end
end
```

### Composing calculations

When we need to performer calculation based on external data we can invoke bindings to `calculated/2`

```elixir
defmodule Sample.Post do
  def create(uuid, performer_uuid) do
    user = Sample.Repo.get(Sample.User, performer_uuid)
    post = %Post{owner_id: uuid}
    load_and_authorize_performer(user)

    permissions do
      calculated(:confirmed_email)
      calculated(:is_owner, [post])
    end
  end

  def confirmed_email(performer) do
    performer.email_confirmed?
  end

  def is_owner(performer, [post]) do
    performer.id == post.owner_id
  end
end
```

To perform exclusive abilities such as `when User is owner of post AND is in editor role` we can do so as in following example

```elixir
defmodule Sample.Post do
  def create(performer_uuid) do
    user = Sample.Repo.get(Sample.User, performer_uuid)
    post = %Post{owner_id: user.id}
    load_and_authorize_performer(user)

    permissions do
      has_role(:editor)
    end

    as_authorized do
      case is_owner(performer, post) do
        :ok -> ...
        {:error, message} -> ...
      end
    end
  end

  def is_owner(performer, post) do
    load_and_authorize_performer(performer)

    permissions do
      calculated(fn p, [post] ->
        p.id == post.owner_id
      end)
    end

    is_authorized?
  end
end
```

We can simplify example in this case by excluding DSL for permissions

```elixir
defmodule Sample.Post do
  def create(uuid, performer_uuid) do
    user = Sample.Repo.get(Sample.User, performer_uuid)
    post = %Post{owner_id: user.id}

    # We can also use has_ability?/2
    if has_role?(user, :admin) and is_owner(user, post) do
      ...
    end
  end

  def is_owner(performer, post) do
    performer.id == post.owner_id
  end
end
```

### Entity related abilities

Terminator allows you to grant abilities on any particular struct. Struct needs to have signature of `%{__struct__: entity_name, id: entity_id}` to infer correct relations. Lets assume that we want to grant `:delete` ability on particular `Post` for our performer:

```elixir
iex> {:ok, performer} = %Terminator.UUID.Performer{} |> Terminator.UUID.Repo.insert()
iex> post = %Post{id: 1}
iex> ability = %Ability{identifier: "delete"}
iex> Terminator.UUID.Performer.grant(performer, :delete, post)
iex> Terminator.UUID.has_ability?(performer, :delete, post)
true
```

```elixir
defmodule Sample.Post do
  def delete(post_id, performer_uuid) do
    user = Sample.Repo.get(Sample.User, performer_uuid)
    post = %Post{id: post_id}
    load_and_authorize_performer(user)

    permissions do
      has_ability(:delete, post)
    end

    as_authorized do
      :ok
    end
  end
end
```

### Granting abilities

Let's assume we want to create new `Role` - _admin_ which is able to delete accounts inside our system. We want to have special `Performer` who is given this _role_ but also he is able to have `Ability` for banning users.

1. Create performer

```elixir
iex> {:ok, performer} = %Terminator.UUID.Performer{} |> Terminator.UUID.Repo.insert()
```

2. Create some abilities

```elixir
iex> {:ok, ability_delete} = Terminator.UUID.Ability.build("delete_accounts", "Delete accounts of users") |> Terminator.UUID.Repo.insert()
iex> {:ok, ability_ban} = Terminator.UUID.Ability.build("ban_accounts", "Ban users") |> Terminator.UUID.Repo.insert()
```

3. Create role

```elixir
iex> {:ok, role} = Terminator.UUID.Role.build("admin", [], "Site administrator") |> Terminator.UUID.Repo.insert()
```

4. Grant abilities to a role

```elixir
iex> Terminator.UUID.Role.grant(role, ability_delete)
```

5. Grant role to a performer

```elixir
iex> Terminator.UUID.Performer.grant(performer, role)
```

6. Grant abilities to a performer

```elixir
iex> Terminator.UUID.Performer.grant(performer, ability_ban)
```

```elixir
iex> performer |> Terminator.UUID.Repo.preload([:roles, :abilities])
%Terminator.UUID.Performer{
  abilities: [
    %Terminator.UUID.Ability{
      identifier: "ban_accounts"
    }
  ]
  roles: [
    %Terminator.UUID.Role{
      identifier: "admin"
      abilities: ["delete_accounts"]
    }
  ]
}
```

### Revoking abilities

Same as we can grant any abilities to models we can also revoke them.

```elixir
iex> Terminator.UUID.Performer.revoke(performer, role)
iex> performer |> Terminator.UUID.Repo.preload([:roles, :abilities])
%Terminator.UUID.Performer{
  abilities: [
    %Terminator.UUID.Ability{
      identifier: "ban_accounts"
    }
  ]
  roles: []
}
iex> Terminator.UUID.Performer.revoke(performer, ability_ban)
iex> performer |> Terminator.UUID.Repo.preload([:roles, :abilities])
%Terminator.UUID.Performer{
  abilities: []
  roles: []
}
```

## License

[MIT © Milos Mosovsky](mailto:milos@mosovsky.com)
[MIT © Jason Clark](mailto:mithereal@gmail.com)