Licet.Validate (Licet v0.1.0)

Copy Markdown View Source

Tests a permission model with a SpiceDB validation file.

A validation file bundles a schema, some relationships, and the answers you expect. Running it tells you whether the model behaves as intended, without a database. Use it in your test suite, or from the command line with mix licet.validate. Testing shows a complete example.

The file is the YAML that play.authzed.com exports and zed validate reads:

  • schema, the .zed text
  • relationships, one relationship per line, with // comments
  • assertions, with assertTrue and assertFalse lists of checks
  • validation, the expected relations: for each resource#permission, every subject and the relations that hold it, as [user:tom] is <document:1#viewer>

Checks run at the current time, so a relationship whose [expiration:...] has passed does not count. Relationships are held in memory; nothing is written to Postgres.

Summary

Types

The parsed validation section: each {type, id, relation} resource with its expected subject lines.

t()

Functions

Computes the validation section from the schema and relationships.

Explains one check against the schema and relationships in the validation YAML.

Parses validation YAML without running it.

Runs validation YAML: every assertion, then every expected relation.

Reads the file at path and runs it with run/1.

Types

expected()

@type expected() :: [{{String.t(), String.t(), String.t()}, [map()]}]

The parsed validation section: each {type, id, relation} resource with its expected subject lines.

failure()

@type failure() ::
  %{assertion: String.t(), expected: :allowed | :denied, got: term()}
  | %{relation: String.t(), message: String.t()}

t()

@type t() :: %Licet.Validate{
  assert_false: [String.t()],
  assert_true: [String.t()],
  relationships: [Licet.Relationship.t()],
  schema: Licet.Schema.t(),
  validation: expected() | nil
}

Functions

expected_relations(source)

@spec expected_relations(String.t()) :: {:ok, String.t()} | {:error, Licet.Error.t()}

Computes the validation section from the schema and relationships.

For each resource#permission key the file already lists, the result lists every subject and the relations that grant it, in the same YAML that the SpiceDB playground writes when it updates expected relations. Review the output once, then commit it to the file.

Examples

iex> Licet.Validate.expected_relations("""
...> schema: |-
...>   definition user {}
...>   definition document {
...>     relation viewer: user
...>     permission view = viewer
...>   }
...> relationships: |-
...>   document:1#viewer@user:ada
...> validation:
...>   document:1#view: []
...> """)
{:ok, "document:1#view:\n    - '[user:ada] is <document:1#viewer>'\n"}

explain(source, line)

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

Explains one check against the schema and relationships in the validation YAML.

line is an assertion string such as "document:1#view@user:ada". The check runs in memory, the same way as run/1, and returns the answer with a trace of the relations evaluated beneath it. Use it from the playground, or when debugging a failing assertion without starting Postgres.

Examples

iex> {:ok, :allowed, trace} =
...>   Licet.Validate.explain(
...>     """
...>     schema: |-
...>       definition user {}
...>       definition document {
...>         relation viewer: user
...>         permission view = viewer
...>       }
...>     relationships: |-
...>       document:1#viewer@user:ada
...>     """,
...>     "document:1#view@user:ada"
...>   )
iex> {trace.resource, trace.permission, trace.result}
{"document:1", "view", :allowed}

parse(source)

@spec parse(String.t()) :: {:ok, t()} | {:error, Licet.Error.t()}

Parses validation YAML without running it.

Examples

iex> {:ok, file} =
...>   Licet.Validate.parse("""
...>   schema: "definition user {}"
...>   assertions:
...>     assertFalse:
...>       - user:ada#member@user:bob
...>   """)
iex> file.assert_false
["user:ada#member@user:bob"]
iex> file.relationships
[]

run(source)

@spec run(String.t()) :: :ok | {:error, Licet.Error.t() | [failure()]}

Runs validation YAML: every assertion, then every expected relation.

Returns :ok, a list of failures, or an error when the file itself is invalid. An assertion failure is a map with the :assertion, the :expected answer, and the answer it :got. An expected-relations failure has the :relation and SpiceDB's :message.

Examples

iex> Licet.Validate.run("""
...> schema: |-
...>   definition user {}
...>   definition document {
...>     relation viewer: user
...>     permission view = viewer
...>   }
...> relationships: |-
...>   document:1#viewer@user:ada
...> assertions:
...>   assertTrue:
...>     - document:1#view@user:ada
...>   assertFalse:
...>     - document:1#view@user:ada
...> """)
{:error, [%{assertion: "document:1#view@user:ada", expected: :denied, got: {:ok, :allowed}}]}

run_file(path)

@spec run_file(Path.t()) :: :ok | {:error, Licet.Error.t() | [failure()]}

Reads the file at path and runs it with run/1.

Examples

test "document permissions" do
  assert :ok = Licet.Validate.run_file("test/permissions/documents.yaml")
end