# `Licet`
[🔗](https://github.com/netoum/licet/blob/v0.1.0/lib/licet.ex#L1)

Relationship-based authorization, stored in your Postgres database.

This module is the whole runtime API: start an instance, write relationships,
and ask questions about them. [Getting started](getting-started.md) walks
through a first setup, and [Concepts](concepts.md) explains the vocabulary.

## Starting an instance

Add an instance to your supervision tree, after the repo it uses:

    children = [
      MyApp.Repo,
      {Licet,
       name: MyApp.Licet,
       repo: MyApp.Repo,
       schema: {:file, Application.app_dir(:my_app, "priv/licet/schema.zed")}}
    ]

The tables must exist first. Create them with `mix licet.gen.migration`.

## Instance names

Every function takes the instance `name` as its first argument. It is the
atom given in the child spec, usually a module-like name such as
`MyApp.Licet`. Choose it in code, never from user input. Calling a function
on a name that isn't running returns `{:error, %Licet.Error{reason: :not_started}}`.

## Options

* `:name` (`t:atom/0`) - Required. The instance name, passed as the first argument of every `Licet` function.

* `:repo` (`t:atom/0`) - Required. The `Ecto.Repo` that holds Licet's tables.

* `:prefix` (`t:String.t/0`) - The Postgres schema that holds the tables. It must match `^[a-z_][a-z0-9_]{0,62}$`. The default value is `"licet"`.

* `:schema` - The schema to load at startup, as `{:file, path}` or `{:string, source}`. When it
  differs from the stored schema, it is stored as a new version, unless the change would
  strand stored relationships. `nil` keeps the stored schema. The default value is `nil`.

* `:quantization` (`t:pos_integer/0`) - Milliseconds between refreshes of the snapshot that `:minimize_latency` reads. The default value is `5000`.

* `:timeout` (`t:pos_integer/0`) - Milliseconds allowed for one check or lookup. The default value is `5000`.

* `:max_depth` (`t:pos_integer/0`) - Arrow and userset hops allowed in one check. The default value is `50`.

* `:max_lookup` (`t:pos_integer/0`) - Results one lookup may compute before failing with `:lookup_too_large`. The default value is `10000`.

* `:max_updates` (`t:pos_integer/0`) - Updates allowed in one `Licet.write/3`. The default value is `1000`.

* `:max_import` (`t:pos_integer/0`) - Relationships allowed in one `Licet.import/3`. The default value is `10000`.

* `:max_preconditions` (`t:pos_integer/0`) - Preconditions allowed on one write. The default value is `1000`.

* `:gc_interval` - Milliseconds between garbage collections, or `false` to disable them. The default value is `300000`.

* `:gc_window` (`t:pos_integer/0`) - Seconds of history to keep. Exact reads of older tokens fail. The default value is `86400`.

* `:gc_batch` (`t:pos_integer/0`) - Rows deleted per garbage collection batch. The default value is `1000`.

## Objects and relationships

Functions take objects and relationships as text, in SpiceDB's format:

  * an object is `type:id`, such as `"document:roadmap"`;
  * a userset subject is `type:id#relation`, such as `"team:eng#member"`;
  * a wildcard subject is `type:*`, such as `"user:*"`;
  * a relationship is `resource#relation@subject`, such as
    `"document:roadmap#viewer@team:eng#member"`, optionally followed by
    `[expiration:2027-01-01T00:00:00Z]`.

Ids are 1 to 1024 characters from `A-Z`, `a-z`, `0-9`, and `/_|=-+`. Type
and relation names are lowercase, 3 to 64 characters, and types may be
namespaced as `prefix/name`, up to 128 characters. `Licet.Relationship`
parses and prints the relationship format.

## Consistency

Writes return a token. Reads take a `:consistency` option: `:minimize_latency`
(the default), `{:at_least, token}`, `{:exact, token}`, or `:full`. See
[Consistency](consistency.md).

## Transactions

Writes join the caller's `Repo.transaction/2` when one is open. A check in
that transaction sees its uncommitted writes. See [Transactions](transactions.md).

## Errors

Every function returns `{:error, %Licet.Error{}}` on failure, with a `reason`
atom you can match on. `Licet.Error` lists the reasons.

# `consistency`

```elixir
@type consistency() ::
  :minimize_latency | {:at_least, token()} | {:exact, token()} | :full
```

Which snapshot of the data a read sees. See [Consistency](consistency.md).

# `filter`

```elixir
@type filter() :: %{
  optional(:resource_type) =&gt; String.t() | [String.t(), ...],
  optional(:resource_id) =&gt; String.t() | [String.t(), ...],
  optional(:relation) =&gt; String.t() | [String.t(), ...],
  optional(:subject_type) =&gt; String.t() | [String.t(), ...],
  optional(:subject_id) =&gt; String.t() | [String.t(), ...],
  optional(:subject_relation) =&gt; String.t() | [String.t(), ...]
}
```

Selects relationships by any of their fields. Fields that are absent match
anything. A list matches any of its values. Other keys, and `nil` values,
are rejected with `:invalid_filter`.

# `instance`

```elixir
@type instance() :: atom()
```

The name of a running instance, as given in its child spec.

# `token`

```elixir
@type token() :: String.t()
```

An opaque consistency token returned by writes.

# `update`

```elixir
@type update() :: {:create | :touch | :delete, String.t() | Licet.Relationship.t()}
```

One relationship change for `write/3`.

# `check`

```elixir
@spec check(instance(), String.t(), String.t(), String.t(), keyword()) ::
  {:ok, :allowed | :denied} | {:error, Licet.Error.t()}
```

Checks whether `subject` has `permission` on `resource`.

`resource` is an object such as `"document:1"`. `permission` is a permission
or relation of the resource's type. `subject` is an object such as
`"user:ada"`, or a userset such as `"team:eng#member"`.

Returns `{:ok, :allowed}` or `{:ok, :denied}`. Treat `{:error, _}` as a
denial.

## Options

  * `:consistency` - which snapshot to read. Defaults to `:minimize_latency`,
    which is cached and can be a few seconds behind the latest write.

## Errors

  * `:invalid_relationship` - malformed object text, or a wildcard resource
  * `:unknown_type`, `:unknown_relation` - not in the schema
  * `:depth_exceeded`, `:cycle` - the evaluation went deeper than `max_depth`
    or revisited itself
  * `:timeout` - longer than the instance `timeout`
  * `:token_invalid`, `:token_not_committed`, `:snapshot_expired` - see
    [Consistency](consistency.md)

## Examples

    Licet.check(MyApp.Licet, "document:1", "view", "user:ada")
    #=> {:ok, :allowed}

    Licet.check(MyApp.Licet, "document:1", "view", "user:bob", consistency: {:at_least, token})
    #=> {:ok, :denied}

# `check_bulk`

```elixir
@spec check_bulk(instance(), [map()], keyword()) ::
  {:ok, [result]} | {:error, Licet.Error.t()}
when result: {:ok, :allowed | :denied} | {:error, Licet.Error.t()}
```

Runs several checks at one snapshot.

Each check is a map with `:resource`, `:permission`, and `:subject`, as in
`check/5`. The results come back in the same order. A failing check,
including an item that isn't such a map, returns its own `{:error, _}`
without stopping the others; the call as a whole fails only when the
snapshot can't be resolved.

All checks read the same snapshot, so their answers are consistent with each
other.

## Options

  * `:consistency` - which snapshot to read. Defaults to `:minimize_latency`.

## Examples

    Licet.check_bulk(MyApp.Licet, [
      %{resource: "document:1", permission: "view", subject: "user:ada"},
      %{resource: "document:2", permission: "view", subject: "user:ada"},
      %{resource: "document:*", permission: "view", subject: "user:ada"}
    ])
    #=> {:ok, [{:ok, :allowed}, {:ok, :denied}, {:error, %Licet.Error{reason: :invalid_relationship}}]}

# `delete`

```elixir
@spec delete(instance(), filter(), keyword()) ::
  {:ok, token()} | {:error, Licet.Error.t()}
```

Deletes every stored relationship that matches `filter` and returns a token.

The filter must include `:resource_type`, so one call can't wipe every
relationship by accident. Use it to clean up after a record is deleted, or
to remove relationships before narrowing the schema.

Deleted relationships stay readable at older snapshots until garbage
collection removes them, after `gc_window`.

Unlike `write/3`, a delete isn't bounded by `max_updates`: it removes every
match in one statement, and it emits no telemetry event. Narrow the filter
when a match could be very large.

## Options

  * `:metadata` - a map stored with the write's transaction record.

## Errors

  * `:invalid_filter` - the filter has no `:resource_type`, names an unknown
    field, or has a value that isn't a string or a list of strings

## Examples

    # Everything about document 1.
    Licet.delete(MyApp.Licet, %{resource_type: "document", resource_id: "1"})

    # Every relationship that names Bob on any document.
    Licet.delete(MyApp.Licet, %{resource_type: "document", subject_type: "user", subject_id: "bob"})

# `explain`

```elixir
@spec explain(instance(), String.t(), String.t(), String.t(), keyword()) ::
  {:ok, :allowed | :denied, map()} | {:error, Licet.Error.t()}
```

Runs a check and returns how it was decided.

Takes the same arguments and options as `check/5`, and returns the answer
with a trace. Each trace node is a map with the `:resource` and
`:permission` it evaluated, its `:result`, and the `:children` evaluated
beneath it.

An allowed trace follows the path that granted access, down to the
relationship that matched. A denied trace is shallower: it lists the
relations checked directly on the resource, but omits the objects an arrow
or userset reached without finding the subject.

Explain never uses the cache and evaluates arrows sequentially, so it's
slower than `check/5`. Use it for debugging.

## Examples

    Licet.explain(MyApp.Licet, "document:1", "view", "user:ada")
    #=> {:ok, :allowed,
    #=>  %{resource: "document:1", permission: "view", result: :allowed, children: [...]}}

# `import`

```elixir
@spec import(instance(), [String.t() | Licet.Relationship.t()], keyword()) ::
  {:ok, token()} | {:error, Licet.Error.t()}
```

Creates many relationships in one transaction and returns a token.

Like `write/3` with only `:create` updates, but limited by the instance
`max_import` (10,000 by default) instead of `max_updates`. Use it to load
existing data, for example when migrating from SpiceDB. The whole batch fails
with `:already_exists` if any relationship is already stored.

Takes the same options and returns the same errors as `write/3`.

## Examples

    Licet.import(MyApp.Licet, [
      "document:1#owner@user:ada",
      "document:2#owner@user:ada",
      "document:2#viewer@team:eng#member"
    ])

# `lookup_resources`

```elixir
@spec lookup_resources(instance(), String.t(), String.t(), String.t(), keyword()) ::
  {:ok, [String.t()], String.t() | nil} | {:error, Licet.Error.t()}
```

Lists the ids of resources of `type` on which `subject` has `permission`.

Use it to filter a list page to what the current user may see. Ids are
returned as strings, sorted, one page at a time. The third element is a
cursor for the next page, or `nil` on the last page.

The full result is computed before a page is returned, and the call fails
with `:lookup_too_large` when it exceeds the instance `max_lookup`.

## Options

  * `:limit` - the page size. Defaults to 100.
  * `:cursor` - the cursor from the previous page.
  * `:consistency` - which snapshot to read. Defaults to `:minimize_latency`.
    Pass the same value for every page.

## Errors

  * `:invalid_relationship` - malformed subject text
  * `:unknown_type`, `:unknown_relation` - not in the schema
  * `:lookup_too_large`, `:depth_exceeded`, `:timeout` - over an instance limit
  * `:invalid_filter` - a malformed cursor, or a `:limit` that isn't a
    positive integer

## Examples

    {:ok, ids, cursor} = Licet.lookup_resources(MyApp.Licet, "document", "view", "user:ada", limit: 2)
    #=> {:ok, ["1", "2"], "v1.IjIi"}

    {:ok, more, nil} =
      Licet.lookup_resources(MyApp.Licet, "document", "view", "user:ada", limit: 2, cursor: cursor)

# `lookup_subjects`

```elixir
@spec lookup_subjects(instance(), String.t(), String.t(), String.t(), keyword()) ::
  {:ok, [%{subject: String.t(), excluded: [String.t()]}], String.t() | nil}
  | {:error, Licet.Error.t()}
```

Lists the subjects of `subject_type` that have `permission` on `resource`.

Use it to show who has access, for example in a sharing dialog.
`subject_type` is an object type such as `"user"`, or a userset type such as
`"team#member"`.

Each result is a map with the `subject` and the subjects it `excluded`. Only
a wildcard excludes anything: `%{subject: "user:*", excluded: ["user:dan"]}`
means every user except Dan. Concrete subjects are listed even when a
wildcard also covers them. A userset counts as a member of itself, so
`"team:eng#member"` is listed for `"team#member"` when the permission
reaches it. This matches SpiceDB's LookupSubjects.

Results are sorted by subject and paginated like `lookup_resources/5`.

## Options

  * `:limit` - the page size. Defaults to 100.
  * `:cursor` - the cursor from the previous page.
  * `:consistency` - which snapshot to read. Defaults to `:minimize_latency`.

## Errors

  * `:invalid_relationship` - malformed resource text
  * `:invalid_filter` - a malformed subject type or cursor, or a `:limit`
    that isn't a positive integer
  * `:unknown_type`, `:unknown_relation` - not in the schema
  * `:lookup_too_large`, `:depth_exceeded`, `:timeout` - over an instance limit

## Examples

    Licet.lookup_subjects(MyApp.Licet, "document:1", "view", "user")
    #=> {:ok, [%{subject: "user:*", excluded: ["user:dan"]}, %{subject: "user:tom", excluded: []}], nil}

    Licet.lookup_subjects(MyApp.Licet, "document:1", "view", "team#member")
    #=> {:ok, [%{subject: "team:eng#member", excluded: []}], nil}

# `read`

```elixir
@spec read(instance(), filter(), keyword()) ::
  {:ok, [Licet.Relationship.t()]} | {:error, Licet.Error.t()}
```

Returns the stored relationships that match `filter`.

The filter must name at least one field. Expired relationships aren't
returned. This reads stored relationships only; to ask whether a subject has
a permission, use `check/5`.

## Options

  * `:consistency` - which snapshot to read. Defaults to `:minimize_latency`.

## Errors

  * `:invalid_filter` - the filter is empty, names an unknown field, or has a
    value that isn't a string or a list of strings
  * `:token_invalid`, `:token_not_committed`, `:snapshot_expired` - see
    [Consistency](consistency.md)

## Examples

    {:ok, relationships} = Licet.read(MyApp.Licet, %{resource_type: "document", resource_id: "1"})
    Enum.map(relationships, &Licet.Relationship.to_string/1)
    #=> ["document:1#owner@user:ada", "document:1#viewer@user:bob"]

# `start_link`

```elixir
@spec start_link(keyword()) :: Supervisor.on_start()
```

Starts an instance and links it to the current process.

Usually you add `{Licet, opts}` to a supervision tree instead of calling this
directly. See the module documentation for the options.

The instance fails to start if the Licet tables don't exist, or if the
configured schema is invalid or would strand stored relationships.

# `write`

```elixir
@spec write(instance(), [update()], keyword()) ::
  {:ok, token()} | {:error, Licet.Error.t()}
```

Applies a batch of relationship updates atomically and returns a token.

Each update is one of:

  * `{:create, relationship}` stores a new relationship and fails with
    `:already_exists` if it is already stored;
  * `{:touch, relationship}` stores it, or updates the expiration of an
    existing one;
  * `{:delete, relationship}` removes it, and does nothing if it isn't stored.

A relationship is text such as `"document:1#viewer@user:ada"` or a
`Licet.Relationship`. Every relationship is validated against the current
schema before anything is written.

All updates apply together or not at all. Inside an open `Repo.transaction/2`,
the write joins it; see [Transactions](transactions.md).

## Options

  * `:preconditions` - a list of `{:must_match, filter}` or
    `{:must_not_match, filter}`, checked inside the write's transaction.
    The write fails with `:precondition_failed` if any doesn't hold.
  * `:metadata` - a map stored with the write's transaction record.

## Errors

  * `:invalid_relationship` - malformed text, or a subject or expiration the
    relation doesn't allow
  * `:unknown_type`, `:unknown_relation` - not in the schema
  * `:already_exists` - a `:create` of a stored relationship
  * `:precondition_failed` - a precondition didn't hold
  * `:too_many_updates`, `:too_many_preconditions` - over `max_updates` or
    `max_preconditions`
  * `:invalid_filter` - `:preconditions` isn't a list, or a precondition
    isn't `{:must_match | :must_not_match, filter}` with a valid filter

## Examples

    {:ok, token} =
      Licet.write(MyApp.Licet, [
        {:create, "document:1#owner@user:ada"},
        {:touch, "document:1#viewer@team:eng#member"},
        {:delete, "document:1#viewer@user:bob"}
      ])

    # Make Ada the owner only if the document has none yet.
    Licet.write(MyApp.Licet, [{:create, "document:1#owner@user:ada"}],
      preconditions: [
        {:must_not_match, %{resource_type: "document", resource_id: "1", relation: "owner"}}
      ]
    )

# `write_schema`

```elixir
@spec write_schema(instance(), String.t(), keyword()) ::
  {:ok, token()} | {:error, Licet.Error.t()}
```

Replaces the schema at runtime and returns a token.

The new schema is parsed, then compared with the stored relationships. It is
rejected with `:unsafe_schema_change` if it would leave some of them
meaningless: a removed definition or relation that still has relationships,
`use expiration` removed while relationships carry an expiry, or a relation
that no longer accepts a subject type it still has. Adding definitions,
relations, permissions, and subject types is always accepted.

Most applications set the schema with the `:schema` option instead, which
applies the same rules at startup. See [Operations](operations.md).

The new schema applies on this node immediately. Other nodes running an
instance on the same tables pick it up within `quantization` milliseconds.
Unlike `write/3`, it can't join an open transaction, because a rollback
would leave nodes using a schema that was never stored.

## Options

  * `:metadata` - a map stored with the write's transaction record.

## Errors

  * `:invalid_schema` - the text doesn't parse; `details` has the line and column
  * `:unsafe_schema_change` - the change would strand stored relationships
  * `:in_transaction` - called inside an open `Repo.transaction/2`

## Examples

    {:ok, token} = Licet.write_schema(MyApp.Licet, File.read!("priv/licet/schema.zed"))

---

*Consult [api-reference.md](api-reference.md) for complete listing*
