provider_digitcount_forbid
A provider gating a forbid rule, alongside a catch-all permit – showing
that a provider call is orthogonal to the rule effect. The same
Strings::DigitCount(context.input.document).count >= 2 atom that permits
Read in provider_digitcount_operator_ge here forbids it, so the verdicts are
the exact inverse:
@id("forbid_digits")
forbid (...) when { Strings::DigitCount(context.input.document).count >= 2 };
@id("permit_read")
permit (...); // catch-all: allow Read by default; the forbid overrides it
Strings::DigitCount(text) -> { count: Long } counts [0-9] matches via the
regex_count host function (see digits.rhai). The provider script is inlined
into providers.json because the CLI reads that file as text and does not
resolve scriptFile references; digits.rhai is kept as the readable source.
The trace over four documents (Cedar’s forbid overrides the catch-all permit):
@0—"abc"(0 digits) → ALLOW@10—"a1b"(1 digit) → ALLOW@20—"a1b2"(2 digits) → DENY@30—"12345"(5 digits) → DENY
Referenced by guide/05-information-providers.md.
Policy
// Forbid Read when the document contains 2 or more digits, as counted by the
// Strings::DigitCount provider. Shows that a provider call is orthogonal to the
// rule effect -- it works the same under forbid as under permit -- with a
// catch-all permit alongside.
@id("forbid_digits")
forbid (
principal,
action == Drupe::Action::"Read",
resource
)
when {
Strings::DigitCount(context.input.document).count >= 2
};
@id("permit_read")
permit (
principal,
action == Drupe::Action::"Read",
resource
);
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: "abc" }) Drupe::Action::"Read"::request(input: { document: "abc" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u1")
@10 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "a1b" }) Drupe::Action::"Read"::request(input: { document: "a1b" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u2")
@20 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "a1b2" }) Drupe::Action::"Read"::request(input: { document: "a1b2" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u3")
@30 scope(principal: Drupe::OAuthUser::"alice", resource: Drupe::Gateway::"gw1") request_context(input: { document: "12345" }) Drupe::Action::"Read"::request(input: { document: "12345" }, callerPrincipal: Drupe::OAuthUser::"alice", callerResource: Drupe::Gateway::"gw1", requestId: "u4")
Expected Output
@0 (time point 0): ALLOW [rules: 1]
@10 (time point 1): ALLOW [rules: 1]
@20 (time point 2): DENY [rules: 0]
@30 (time point 3): DENY [rules: 0]
Provider Declarations
{
"availableProviders": {
"Strings::DigitCount": {
"argumentTypes": [
{
"paramType": "string"
}
],
"outputType": {
"paramType": "record",
"fields": {
"count": {
"paramType": "integer"
}
},
"required": [
"count"
]
},
"implementation": {
"kind": "rhai",
"script": "// Strings::DigitCount(text) -> { count: Long }.\n// Counts the digit characters in the document via the regex_count host\n// function (non-overlapping matches of [0-9]).\n// (Kept in sync with digits.rhai; inlined here because the CLI reads\n// providers.json as text and does not resolve scriptFile references.)\nfn 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 #{ count: -1 };\n }\n\n #{ count: regex_count(\"[0-9]\", text) }\n}\n"
}
}
}
}