Setup
Migration
mix licet.gen.migration
mix ecto.migrate
Supervision tree
{Licet,
name: MyApp.Licet,
repo: MyApp.Repo,
schema: {:file, Application.app_dir(:my_app, "priv/licet/schema.zed")}}Schema
Definitions
definition user {}
definition team {
relation member: user
}
definition document {
relation parent: folder
relation owner: user
relation viewer: user | user:* | team#member
permission edit = owner + parent->edit
permission view = viewer + edit
}Operators
| Syntax | Meaning |
|---|---|
a + b | either grants |
a & b | both grant |
a - b | a grants and b doesn't |
rel->perm | perm on any object in rel |
rel.any(perm) | same as rel->perm |
rel.all(perm) | perm on every object in rel |
nil | nothing |
user:* | every user |
team#member | members of a team |
Precedence, tightest first: arrows, +, &, -.
Expiration
use expiration
definition document {
relation viewer: user with expiration
}Relationships
Text form
| Text | Meaning |
|---|---|
document:1#owner@user:ada | Ada owns document 1 |
document:1#viewer@team:eng#member | team eng's members view it |
document:1#viewer@user:* | every user views it |
document:1#viewer@user:bob[expiration:2027-01-01T00:00:00Z] | until 2027 |
Writes
{:ok, token} =
Licet.write(MyApp.Licet, [
{:create, "document:1#owner@user:ada"},
{:touch, "document:1#viewer@user:bob"},
{:delete, "document:1#viewer@user:cara"}
]):create fails if the relationship exists. :touch creates or updates it. :delete removes it.
Delete by filter
Licet.delete(MyApp.Licet, %{resource_type: "document", resource_id: "1"})Read
{:ok, relationships} =
Licet.read(MyApp.Licet, %{resource_type: "document", resource_id: "1"})Queries
Check
Licet.check(MyApp.Licet, "document:1", "view", "user:ada")
#=> {:ok, :allowed}Bulk check
Licet.check_bulk(MyApp.Licet, [
%{resource: "document:1", permission: "view", subject: "user:ada"},
%{resource: "document:2", permission: "view", subject: "user:ada"}
])
#=> {:ok, [{:ok, :allowed}, {:ok, :denied}]}Lookup resources
{:ok, ids, cursor} =
Licet.lookup_resources(MyApp.Licet, "document", "view", "user:ada", limit: 100)Lookup subjects
{:ok, subjects, cursor} =
Licet.lookup_subjects(MyApp.Licet, "document:1", "view", "user")
#=> [%{subject: "user:*", excluded: ["user:dan"]}, ...]Explain
{:ok, :allowed, trace} = Licet.explain(MyApp.Licet, "document:1", "view", "user:ada")Consistency
Modes
| Option | Sees |
|---|---|
:minimize_latency | a shared snapshot, cached (default) |
{:at_least, token} | at least the write behind token |
{:exact, token} | exactly the snapshot of token |
:full | the latest committed data, uncached |
Usage
Licet.check(MyApp.Licet, "document:1", "view", "user:ada",
consistency: {:at_least, token}
)Errors
Shape
{:error, %Licet.Error{reason: :unknown_relation, message: message}}Common reasons
| Reason | Cause |
|---|---|
:invalid_relationship | malformed tuple text |
:unknown_type | type not in the schema |
:unknown_relation | relation or permission not in the schema |
:already_exists | :create of an existing tuple |
:timeout | the check took longer than :timeout |
:unsafe_schema_change | stored tuples would be stranded |
See Licet.Error for the full list.