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 (hasReadwithReadInput = { document: String }), lifted fromprovider_only/corpus/0004_two_providers_and_not.providers.json— declaresStrings::MatchesandLists::Blocked. Each Rhai body is inlined in thescriptfield (rather than referenced viascriptFile) because thedogwoodCLI reads--providersas text withProviderDeclarations::from_json, which does not resolve externalscriptFilereferences at replay time.matches.rhai— theStrings::Matchesbody kept as a readable standalone source (lifted from the corpus).blocked.rhai— theLists::Blockedbody kept as a readable standalone source (a tiny hard-coded denylist:"evil","badword").trace.log— threeReadevents; see verdicts below.expected.out— captured from the realdogwood replayrun.
Verdicts (from dogwood replay)
@0—document: "hello"→ ALLOW: matches^[a-z]+$and is not on the blocklist.@10—document: "evil"→ DENY: matches the pattern but is blocked; the!rejects it.@20—document: "Hello"→ DENY: the uppercaseHfails 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"
}
}
}
}