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_regex_analyze_fields

Several calls to the same Regex::Analyze provider, each projecting a different output field and comparing it, combined with && as plain Cedar (the unwrapped provider form — no guardrails { … } block). Permit Read only when the document:

  1. starts with an uppercase letter — ^[A-Z].is_match == true
  2. has at least three digit characters — [0-9].count >= 3
  3. whose first run of digits is exactly 42[0-9]+.first_match == "42"

Regex::Analyze(string, string) -> { is_match: bool, first_match: string, count: integer } is declared in providers.json. Its Rhai script is inlined into providers.json (as the "script" field) rather than referenced via scriptFile, because the dogwood CLI parses the declarations text directly and does not resolve external scriptFile references.

Trace

Each denial isolates one failing condition:

docstarts A–Z≥3 digitsfirst run == “42”verdict
Abc42x999yesyes (5)yesALLOW
abc42x999noyesyesDENY
A42yesno (2)yesDENY
A99942yesyesno (99942)DENY

Run from this directory so the schema/providers 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

Lifted from corpus case 0005_regex_operations.

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

Policy

// Permit Read only when the document starts with an uppercase letter, has at
// least three digit characters, and its first run of digits is exactly "42".
// Three calls to one Regex::Analyze provider, each projecting a different
// output field, combined with && as plain Cedar.
@id("read_regex_analyze")
permit (
    principal,
    action == Drupe::Action::"Read",
    resource
)
when {
    Regex::Analyze(context.input.document, "^[A-Z]").is_match == true
    && Regex::Analyze(context.input.document, "[0-9]").count >= 3
    && Regex::Analyze(context.input.document, "[0-9]+").first_match == "42"
};

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: "Abc42x999" }) Drupe::Action::"Read"::request(input: { document: "Abc42x999" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@10 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "abc42x999" }) Drupe::Action::"Read"::request(input: { document: "abc42x999" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@20 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "A42" }) Drupe::Action::"Read"::request(input: { document: "A42" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@30 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "A99942" }) Drupe::Action::"Read"::request(input: { document: "A99942" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u4")

Expected Output

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

Provider Declarations

{
  "availableProviders": {
    "Regex::Analyze": {
      "argumentTypes": [
        {
          "paramType": "string"
        },
        {
          "paramType": "string"
        }
      ],
      "outputType": {
        "paramType": "record",
        "fields": {
          "is_match": {
            "paramType": "bool"
          },
          "first_match": {
            "paramType": "string"
          },
          "count": {
            "paramType": "integer"
          }
        },
        "required": [
          "is_match",
          "first_match",
          "count"
        ]
      },
      "implementation": {
        "kind": "rhai",
        "script": "// `Regex::Analyze(text, pattern) -> { is_match, first_match, count }`.\n//\n// One provider that exercises ALL THREE regex host functions Dogwood\n// exposes to provider scripts:\n//   * regex_is_match(pattern, text) -> bool   \u2014 does the pattern match?\n//   * regex_find(pattern, text)     -> string \u2014 the first match (\"\" if none)\n//   * regex_count(pattern, text)    -> i64     \u2014 number of matches\n//\n// The output record surfaces each result so the policy can project and\n// compare them independently.\nfn 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 #{ is_match: false, first_match: \"\", count: -1 };\n    }\n\n    #{\n        is_match:    regex_is_match(pattern, text),\n        first_match: regex_find(pattern, text),\n        count:       regex_count(pattern, text),\n    }\n}\n"
      }
    }
  }
}