assay-lua 0.19.1

General-purpose enhanced Lua runtime. Batteries-included scripting, automation, and web services.
Documentation
mod common;

use common::run_lua;

const VOCABULARY: &str = r#"
local authz = require("assay.authz")
local eng = authz.engine({
  condition_keys = { ["app:Region"] = { type = "string" } },
  scope_kinds = { "root", "space" },
  default_scope_chain = { { kind = "root", id = "*" } },
  actions = {
    { action = "docs.read" },
    { action = "docs.write", derives_from = "docs.read" },
  },
  grants = {
    {
      subject = { kind = "user", id = "alice" },
      scope = { kind = "root", id = "*" },
      statements = {
        { effect = "allow", actions = { "docs.read" }, resources = { "doc:*" } },
      },
    },
    {
      subject = { kind = "user", id = "alice" },
      scope = { kind = "root", id = "*" },
      statements = {
        { effect = "deny", actions = { "docs.read" }, resources = { "doc:secret" } },
      },
    },
    {
      subject = { kind = "user", id = "bounded" },
      scope = { kind = "root", id = "*" },
      statements = {
        { effect = "allow", actions = { "docs.read" }, resources = { "doc:*" } },
      },
      bounds = {
        { operator = "StringEquals", key = "app:Region", value = "eu-west" },
      },
    },
  },
})
local alice = { { kind = "user", id = "alice" } }
local bounded = { { kind = "user", id = "bounded" } }
"#;

async fn run(body: &str) {
    run_lua(&format!("{VOCABULARY}\n{body}"))
        .await
        .unwrap_or_else(|error| panic!("lua failed: {error}"));
}

#[tokio::test]
async fn an_allow_statement_allows() {
    run(r#"
        local d = eng:check(alice, "docs.read", "doc:42")
        assert.eq(d.allowed, true)
        assert.eq(d.decision, "allow")
        assert.eq(d.reason, "allowed")
    "#)
    .await;
}

#[tokio::test]
async fn a_deny_beats_an_allow_from_another_grant() {
    run(r#"
        local d = eng:check(alice, "docs.read", "doc:secret")
        assert.eq(d.allowed, false)
        assert.eq(d.reason, "explicit_deny")
    "#)
    .await;
}

#[tokio::test]
async fn nothing_granted_denies() {
    run(r#"
        local d = eng:check({ { kind = "user", id = "nobody" } }, "docs.read", "doc:42")
        assert.eq(d.allowed, false)
        assert.eq(d.reason, "no_matching_grant")
    "#)
    .await;
}

#[tokio::test]
async fn a_grant_bound_confines_the_allow_it_carries() {
    run(r#"
        local inside = eng:check(bounded, "docs.read", "doc:42", {
          context = { ["app:Region"] = "eu-west" },
        })
        assert.eq(inside.allowed, true)

        local outside = eng:check(bounded, "docs.read", "doc:42", {
          context = { ["app:Region"] = "us-east" },
        })
        assert.eq(outside.allowed, false)

        local unpopulated = eng:check(bounded, "docs.read", "doc:42")
        assert.eq(unpopulated.allowed, false)
    "#)
    .await;
}

#[tokio::test]
async fn a_statement_naming_a_base_action_covers_what_derives_from_it() {
    run(r#"
        local derived = eng:check(alice, "docs.write", "doc:42")
        assert.eq(derived.allowed, true)
    "#)
    .await;
}

#[tokio::test]
async fn a_malformed_scope_chain_denies_outright() {
    run(r#"
        local d = eng:check(alice, "docs.read", "doc:42", {
          scope_chain = { { kind = "root", id = "*" }, { kind = "galaxy", id = "x" } },
        })
        assert.eq(d.allowed, false)
        assert.eq(d.reason, "undeclared_scope_kind")
    "#)
    .await;
}

#[tokio::test]
async fn validate_refuses_a_condition_the_engine_could_never_evaluate() {
    run(r#"
        local ok = eng:validate({
          { effect = "allow", actions = { "docs.read" }, resources = { "doc:*" },
            conditions = { { operator = "StringEquals", key = "app:Region", value = "eu-west" } } },
        })
        assert.not_nil(ok)

        local bad, err = eng:validate({
          { effect = "allow", actions = { "docs.read" }, resources = { "doc:*" },
            conditions = { { operator = "Bogus", key = "app:Region", value = "eu-west" } } },
        })
        assert.eq(bad, nil)
        assert.contains(err, "unknown operator")

        local unknown_action, action_err = eng:validate({
          { effect = "allow", actions = { "docs.destroy" }, resources = { "doc:*" } },
        })
        assert.eq(unknown_action, nil)
        assert.contains(action_err, "unknown or wildcard action")
    "#)
    .await;
}

#[tokio::test]
async fn describe_serves_the_declared_vocabulary_as_data() {
    run(r#"
        local d = eng:describe()
        assert.eq(d.version, 1)
        assert.eq(#d.actions, 2)
        assert.eq(d.actions[1].action, "docs.read")
        assert.eq(d.actionClosures["docs.read"][1], "docs.write")
        assert.eq(d.conditionKeys["request:Time"].builtIn, true)
        assert.eq(d.conditionKeys["app:Region"].type, "string")
        assert.eq(#d.scopeKinds, 2)
    "#)
    .await;
}

#[tokio::test]
async fn grants_for_lists_what_applies_over_a_chain() {
    run(r#"
        local grants = eng:grants_for(alice)
        assert.eq(#grants, 2)
        local none = eng:grants_for({ { kind = "user", id = "nobody" } })
        assert.eq(#none, 0)
    "#)
    .await;
}

const CONTEXT_SHAPES: &str = r#"
local authz = require("assay.authz")
local eng = authz.engine({
  condition_keys = {
    ["app:Roles"] = { type = "string" },
    ["app:Flag"] = { type = "string" },
    ["app:Str"] = { type = "string" },
  },
  scope_kinds = { "root" },
  default_scope_chain = { { kind = "root", id = "*" } },
  grants = {
    {
      subject = { kind = "user", id = "alice" },
      scope = { kind = "root", id = "*" },
      statements = {
        { effect = "allow", actions = { "docs.read" }, resources = { "*" } },
        { effect = "deny", actions = { "docs.read" }, resources = { "*" },
          conditions = { { operator = "StringNotIn", key = "app:Roles", values = { "admin" } } } },
      },
    },
  },
})
local gated = authz.engine({
  condition_keys = {
    ["app:Flag"] = { type = "string" },
    ["app:Str"] = { type = "string" },
  },
  scope_kinds = { "root" },
  default_scope_chain = { { kind = "root", id = "*" } },
  grants = {
    {
      subject = { kind = "user", id = "alice" },
      scope = { kind = "root", id = "*" },
      statements = {
        { effect = "allow", actions = { "docs.read" }, resources = { "*" },
          conditions = { { operator = "StringLike", key = "app:Flag", value = "tr*" } } },
      },
    },
  },
})
local alice = { { kind = "user", id = "alice" } }
"#;

async fn run_shapes(body: &str) {
    run_lua(&format!("{CONTEXT_SHAPES}\n{body}"))
        .await
        .unwrap_or_else(|error| panic!("lua failed: {error}"));
}

#[tokio::test]
async fn an_empty_context_table_decides_instead_of_aborting_the_check() {
    run_shapes(
        r#"
        local held_none = eng:check(alice, "docs.read", "doc:1", {
          context = { ["app:Roles"] = {} },
        })
        assert.eq(held_none.allowed, false)
        assert.eq(held_none.reason, "explicit_deny")

        local held_admin = eng:check(alice, "docs.read", "doc:1", {
          context = { ["app:Roles"] = { "admin" } },
        })
        assert.eq(held_admin.allowed, true)
    "#,
    )
    .await;
}

#[tokio::test]
async fn a_sparse_context_table_decides_instead_of_aborting_the_check() {
    run_shapes(
        r#"
        local sparse = eng:check(alice, "docs.read", "doc:1", {
          context = { ["app:Roles"] = { "admin", nil, "sre" } },
        })
        assert.eq(sparse.allowed, true)
    "#,
    )
    .await;
}

#[tokio::test]
async fn a_boolean_context_value_decides_instead_of_aborting_the_check() {
    run_shapes(
        r#"
        local truthy = gated:check(alice, "docs.read", "doc:1", {
          context = { ["app:Flag"] = true },
        })
        assert.eq(truthy.allowed, true)

        local falsy = gated:check(alice, "docs.read", "doc:1", {
          context = { ["app:Flag"] = false },
        })
        assert.eq(falsy.allowed, false)

        local numeric = gated:check(alice, "docs.read", "doc:1", {
          context = { ["app:Flag"] = 12 },
        })
        assert.eq(numeric.allowed, false)
    "#,
    )
    .await;
}

#[tokio::test]
async fn validate_bounds_gates_a_write_the_evaluator_would_refuse() {
    run_shapes(
        r#"
        local ok = eng:validate_bounds({
          { operator = "StringEquals", key = "app:Str", value = "x" },
        })
        assert.not_nil(ok)

        local bad, err = eng:validate_bounds({
          { operator = "DateLessThan", key = "request:Time", value = "2027-01-01T00:00:00" },
        })
        assert.eq(bad, nil)
        assert.contains(err, "timezone")
    "#,
    )
    .await;
}