Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

provider_matches_and_not_blocked

Two information providers combined with the boolean spine (&& and !) inside one ordinary when { ... } clause: permit Read only when the document matches an allowed-name pattern (Strings::Matches against ^[a-z]+$) and is not on the blocklist (!(Lists::Blocked(...).blocked == true)).

A when { ... } body is a boolean expression over multiple provider atoms, not just one; each provider is a plain namespaced call recognized and hoisted at lowering.

Files

  • policy.dw — the permit rule combining both providers with && and !.
  • schema.cedarschema — the minimal Drupe action schema (has Read with ReadInput = { document: String }), lifted from provider_only/corpus/0004_two_providers_and_not.
  • providers.json — declares Strings::Matches and Lists::Blocked. Each Rhai body is inlined in the script field (rather than referenced via scriptFile) because the dogwood CLI reads --providers as text with ProviderDeclarations::from_json, which does not resolve external scriptFile references at replay time.
  • matches.rhai — the Strings::Matches body kept as a readable standalone source (lifted from the corpus).
  • blocked.rhai — the Lists::Blocked body kept as a readable standalone source (a tiny hard-coded denylist: "evil", "badword").
  • trace.log — three Read events; see verdicts below.
  • expected.out — captured from the real dogwood replay run.

Verdicts (from dogwood replay)

  • @0document: "hello"ALLOW: matches ^[a-z]+$ and is not on the blocklist.
  • @10document: "evil"DENY: matches the pattern but is blocked; the ! rejects it.
  • @20document: "Hello"DENY: the uppercase H fails the lowercase-only pattern.

Reproduce

Run from this directory (so relative provider paths resolve):

dogwood validate policy.dw --policy-schema schema.cedarschema --providers providers.json
dogwood replay   policy.dw --policy-schema schema.cedarschema --providers providers.json --trace trace.log

Referenced by guide/05-information-providers.md.

Policy

// Permit Read only when the document matches an allowed-name pattern AND is
// not on the blocklist. Two information providers combined with the boolean
// spine (&& and !) inside one ordinary when { ... } clause.
@id("read_allowed_and_not_blocked")
permit (
    principal,
    action == Drupe::Action::"Read",
    resource
)
when {
    Strings::Matches(context.input.document, "^[a-z]+$").matched == true
    && !(Lists::Blocked(context.input.document).blocked == true)
};

Schema

namespace Drupe {
  type ReadInput = {
    document: String
  };

  entity Gateway;

  entity OAuthUser = {
    id: String
  } tags String;

  action "Read" appliesTo {
    principal: [OAuthUser],
    resource: [Gateway],
    context: {
      input: ReadInput
    }
  };
}

Trace

@0 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "hello" }) Drupe::Action::"Read"::request(input: { document: "hello" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@10 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "evil" }) Drupe::Action::"Read"::request(input: { document: "evil" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@20 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "Hello" }) Drupe::Action::"Read"::request(input: { document: "Hello" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u3")

Expected Output

@0 (time point 0): ALLOW  [rules: 0]
@10 (time point 1): DENY
@20 (time point 2): DENY

Provider Declarations

{
  "availableProviders": {
    "Strings::Matches": {
      "argumentTypes": [
        {
          "paramType": "string"
        },
        {
          "paramType": "string"
        }
      ],
      "outputType": {
        "paramType": "record",
        "fields": {
          "matched": {
            "paramType": "bool"
          }
        },
        "required": [
          "matched"
        ]
      },
      "implementation": {
        "kind": "rhai",
        "script": "fn evaluate(text, pattern) {\n    // Defensive per the provider contract: a provider may be evaluated\n    // for ANY decision event, so any argument may be absent (unit).\n    // Return a conforming sentinel instead of erroring (errors are UB).\n    if type_of(text) == \"()\" || type_of(pattern) == \"()\" {\n        return #{ matched: false };\n    }\n\n    #{ matched: regex_is_match(pattern, text) }\n}\n"
      }
    },
    "Lists::Blocked": {
      "argumentTypes": [
        {
          "paramType": "string"
        }
      ],
      "outputType": {
        "paramType": "record",
        "fields": {
          "blocked": {
            "paramType": "bool"
          }
        },
        "required": [
          "blocked"
        ]
      },
      "implementation": {
        "kind": "rhai",
        "script": "fn evaluate(text) {\n    // Defensive per the provider contract: a provider may be evaluated\n    // for ANY decision event, so any argument may be absent (unit).\n    // Return a conforming sentinel instead of erroring (errors are UB).\n    if type_of(text) == \"()\" {\n        return #{ blocked: false };\n    }\n\n    let denylist = [\"evil\", \"badword\"];\n    #{ blocked: denylist.contains(text) }\n}\n"
      }
    }
  }
}