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_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"
      }
    }
  }
}