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_enum_severity_forbid

A provider that returns a value from a fixed list. Vendor::ThreatScan(text) returns { severity }, where severity is the enum Vendor::Severity (LOW, MEDIUM, HIGH, CRITICAL), declared under entityTypes in providers.json. A forbid blocks HIGH and CRITICAL, alongside a catch-all permit:

@id("forbid_severe")
forbid (...) when {
    Vendor::ThreatScan(context.input.document).severity
        in [Vendor::Severity::"HIGH", Vendor::Severity::"CRITICAL"]
};

The script (threat_scan.rhai, inlined into providers.json) returns the bare id, and Dogwood converts it to the enum value. The trace:

  • @0 "hello" (LOW) → ALLOW
  • @10 "port scan" (MEDIUM) → ALLOW
  • @20 "malware here" (HIGH) → DENY by forbid_severe
  • @30 "ransomware" (CRITICAL) → DENY by forbid_severe
  • @40 "garbled": the script returns SEVERE, which isn’t a declared value → DENY (fail closed), with `SEVERE` is not a member of `Vendor::Severity`

Misspelling a value in the policy (Vendor::Severity::"CRITCAL") fails dogwood validate. With a plain string field, the same typo would validate and the forbid would never fire.

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

Policy

@id("permit_read")
permit (principal, action == Drupe::Action::"Read", resource);

// Block high and critical threats. `Vendor::Severity::"HIGH"` is checked
// against the declared values: a typo such as "HIHG" is a validation error.
@id("forbid_severe")
forbid (principal, action == Drupe::Action::"Read", resource)
when {
    Vendor::ThreatScan(context.input.document).severity
        in [Vendor::Severity::"HIGH", Vendor::Severity::"CRITICAL"]
};

Schema

namespace Drupe {
  type ReadInput = { document: String };
  entity Gateway;
  entity OAuthUser;
  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: "port scan" }) Drupe::Action::"Read"::request(input: { document: "port scan" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@20 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "malware here" }) Drupe::Action::"Read"::request(input: { document: "malware here" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@30 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "ransomware" }) Drupe::Action::"Read"::request(input: { document: "ransomware" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u4")
@40 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "garbled" }) Drupe::Action::"Read"::request(input: { document: "garbled" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u5")

Expected Output

@0 (time point 0): ALLOW  [rules: 0]
@10 (time point 1): ALLOW  [rules: 0]
@20 (time point 2): DENY  [rules: 1]
@30 (time point 3): DENY  [rules: 1]
@40 (time point 4): DENY
    error: provider `Vendor::ThreatScan`.severity: `SEVERE` is not a member of `Vendor::Severity`

Provider Declarations

{
  "entityTypes": {
    "Vendor::Severity": {
      "enum": [
        "LOW",
        "MEDIUM",
        "HIGH",
        "CRITICAL"
      ]
    }
  },
  "availableProviders": {
    "Vendor::ThreatScan": {
      "argumentTypes": [
        {
          "paramType": "string"
        }
      ],
      "outputType": {
        "paramType": "record",
        "fields": {
          "severity": {
            "paramType": "entity",
            "entityType": "Vendor::Severity"
          }
        },
        "required": [
          "severity"
        ]
      },
      "implementation": {
        "kind": "rhai",
        "script": "// Vendor::ThreatScan(text) -> { severity: Vendor::Severity }.\n// Returns the bare enum id; Dogwood converts it to the enum entity and denies\n// the request if the id isn't one of the declared values.\nfn evaluate(text) {\n    // A provider may be evaluated for ANY decision event, so the argument may\n    // be absent (unit). Return the most restrictive value.\n    if type_of(text) == \"()\" { return #{ severity: \"CRITICAL\" }; }\n    let t = to_lower(text);\n    if t.contains(\"ransomware\") { #{ severity: \"CRITICAL\" } }\n    else if t.contains(\"malware\") { #{ severity: \"HIGH\" } }\n    else if t.contains(\"scan\") { #{ severity: \"MEDIUM\" } }\n    else if t.contains(\"garbled\") { #{ severity: \"SEVERE\" } }   // not a declared value\n    else { #{ severity: \"LOW\" } }\n}\n"
      }
    }
  }
}