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.zedtextrelationships, one relationship per line, with//commentsassertions, withassertTrueandassertFalselists of checksvalidation, the expected relations: for eachresource#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.
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
The parsed validation section: each {type, id, relation} resource with
its expected subject lines.
@type t() :: %Licet.Validate{ assert_false: [String.t()], assert_true: [String.t()], relationships: [Licet.Relationship.t()], schema: Licet.Schema.t(), validation: expected() | nil }
Functions
@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"}
@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}
@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
[]
@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}}]}
@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