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 byforbid_severe@30"ransomware"(CRITICAL) → DENY byforbid_severe@40"garbled": the script returnsSEVERE, 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"
}
}
}
}