Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/MilosMosovsky/terminator
🛡 Modern elixir ACL/ABAC library for managing granular user abilities and permissions
https://github.com/MilosMosovsky/terminator
acl ecto elixir elixir-phoenix
Last synced: 8 days ago
JSON representation
🛡 Modern elixir ACL/ABAC library for managing granular user abilities and permissions
- Host: GitHub
- URL: https://github.com/MilosMosovsky/terminator
- Owner: MilosMosovsky
- Created: 2019-01-02T12:04:05.000Z (almost 6 years ago)
- Default Branch: master
- Last Pushed: 2023-10-26T01:27:09.000Z (about 1 year ago)
- Last Synced: 2024-10-02T12:17:59.573Z (about 1 month ago)
- Topics: acl, ecto, elixir, elixir-phoenix
- Language: Elixir
- Homepage: https://hexdocs.pm/terminator/
- Size: 43.9 KB
- Stars: 61
- Watchers: 5
- Forks: 12
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
- freaking_awesome_elixir - Elixir - Database based authorization (ACL), with custom DSL rules for requiring needed permissions. ([Docs](https://hexdocs.pm/terminator/readme.html)). (Authorization)
- fucking-awesome-elixir - terminator - Database based authorization (ACL), with custom DSL rules for requiring needed permissions. ([Docs](https://hexdocs.pm/terminator/readme.html)). (Authorization)
- awesome-elixir - terminator - Database based authorization (ACL), with custom DSL rules for requiring needed permissions. ([Docs](https://hexdocs.pm/terminator/readme.html)). (Authorization)
README
# 🛡 Terminator 🛡
[![Coverage Status](https://img.shields.io/coveralls/github/MilosMosovsky/terminator.svg?style=flat-square)](https://coveralls.io/github/MilosMosovsky/terminator)
[![Build Status](https://img.shields.io/travis/MilosMosovsky/terminator.svg?style=flat-square)](https://travis-ci.org/MilosMosovsky/terminator)
[![Version](https://img.shields.io/hexpm/v/terminator.svg?style=flat-square)](https://hex.pm/packages/terminator)Terminator is toolkit for granular ability management for performers. 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 Terminatordef delete_post(id) do
performer = Sample.Repo.get(Terminator.Performer, 1)
load_and_authorize_performer(performer)
post = %Post{id: 1}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)
endas_authorized do
Sample.Repo.get(Sample.Post, id) |> Sample.repo.delete()
end# Notice that you can use both macros or functions
case is_authorized? do
:ok -> Sample.Repo.get(Sample.Post, id) |> 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, "~> 0.5.2"}
]
end
``````elixir
# In your config/config.exs file
config :terminator, Terminator.Repo,
username: "postgres",
password: "postgres",
database: "terminator_dev",
hostname: "localhost"
``````elixir
iex> mix terminator.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.Migrationdef change do
create table(:users) do
add :username, :string
add :performer_id, references(Terminator.Performer.table())timestamps()
endcreate 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.Performer.changeset/2`) and call `put_assoc` inside your changeset
```elixir
# In schema defintion
defmodule Sample.User do
use Ecto.Schemaschema "users" do
field :username, :Stringbelongs_to :performer, Terminator.Performer
timestamps()
end
end
``````elixir
# In your model
defmodule Sample.Post
use Terminatordef delete_post(id) do
user = Sample.Repo.get(Sample.User, 1)
load_and_authorize_performer(user)
# Function allows multiple signatues of performer it can
# be either:
# * %Terminator.Performer{}
# * %AnyStruct{performer: %Terminator.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
endas_authorized do
Sample.Repo.get(Sample.Post, id) |> Sample.repo.delete()
end# Notice that you can use both macros or functions
case is_authorized? do
:ok -> Sample.Repo.get(Sample.Post, id) |> 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() do
user = Sample.Repo.get(Sample.User, 1)
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() do
user = Sample.Repo.get(Sample.User, 1)
load_and_authorize_performer(user)permissions do
calculated(:confirmed_email)
end
enddef 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() do
user = Sample.Repo.get(Sample.User, 1)
post = %Post{owner_id: 1}
load_and_authorize_performer(user)permissions do
calculated(:confirmed_email)
calculated(:is_owner, [post])
end
enddef confirmed_email(performer) do
performer.email_confirmed?
enddef 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() do
user = Sample.Repo.get(Sample.User, 1)
post = %Post{owner_id: 1}
load_and_authorize_performer(user)permissions do
has_role(:editor)
endas_authorized do
case is_owner(performer, post) do
:ok -> ...
{:error, message} -> ...
end
end
enddef is_owner(performer, post) do
load_and_authorize_performer(performer)permissions do
calculated(fn p, [post] ->
p.id == post.owner_id
end)
endis_authorized?
end
end
```We can simplify example in this case by excluding DSL for permissions
```elixir
defmodule Sample.Post do
def create() do
user = Sample.Repo.get(Sample.User, 1)
post = %Post{owner_id: 1}# We can also use has_ability?/2
if has_role?(user, :admin) and is_owner(user, post) do
...
end
enddef 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.Performer{} |> Terminator.Repo.insert()
iex> post = %Post{id: 1}
iex> ability = %Ability{identifier: "delete"}
iex> Terminator.Performer.grant(performer, :delete, post)
iex> Terminator.has_ability?(performer, :delete, post)
true
``````elixir
defmodule Sample.Post do
def delete() do
user = Sample.Repo.get(Sample.User, 1)
post = %Post{id: 1}
load_and_authorize_performer(user)permissions do
has_ability(:delete, post)
endas_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.Performer{} |> Terminator.Repo.insert()
```2. Create some abilities
```elixir
iex> {:ok, ability_delete} = Terminator.Ability.build("delete_accounts", "Delete accounts of users") |> Terminator.Repo.insert()
iex> {:ok, ability_ban} = Terminator.Ability.build("ban_accounts", "Ban users") |> Terminator.Repo.insert()
```3. Create role
```elixir
iex> {:ok, role} = Terminator.Role.build("admin", [], "Site administrator") |> Terminator.Repo.insert()
```4. Grant abilities to a role
```elixir
iex> Terminator.Role.grant(role, ability_delete)
```5. Grant role to a performer
```elixir
iex> Terminator.Performer.grant(performer, role)
```6. Grant abilities to a performer
```elixir
iex> Terminator.Performer.grant(performer, ability_ban)
``````elixir
iex> performer |> Terminator.Repo.preload([:roles, :abilities])
%Terminator.Performer{
abilities: [
%Terminator.Ability{
identifier: "ban_accounts"
}
]
roles: [
%Terminator.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.Performer.revoke(performer, role)
iex> performer |> Terminator.Repo.preload([:roles, :abilities])
%Terminator.Performer{
abilities: [
%Terminator.Ability{
identifier: "ban_accounts"
}
]
roles: []
}
iex> Terminator.Performer.revoke(performer, ability_ban)
iex> performer |> Terminator.Repo.preload([:roles, :abilities])
%Terminator.Performer{
abilities: []
roles: []
}
```## License
[MIT © Milos Mosovsky](mailto:[email protected])