Information Providers
This page covers using Dogwood’s information providers: values computed at
authorize time by a small piece of sandboxed code, then folded back into a policy as
if they had always been part of the request context. It explains how you call a
provider from an ordinary when { … } clause, the arguments a provider can take, how
its output composes with the rest of a condition, and how that output reaches you as
context.providers.<id>. Corpus cases named below are directories under
dogwood-language/tests/passing/provider_only/corpus/; each complete policy
shown below is also a runnable bundle under examples/.
This page takes the providers themselves as given. Declaring one — the
providers.json format, the Rhai implementation contract (the sandbox, host
functions, decimal support, the off-by-default net feature), and the advanced
features (output methods, no-implementation providers, and the guardrails { … }
sugar) — is the subject of the Advanced-topics page
The provider schema.
What an information provider is, and why
Cedar policies decide on the request they are given. Sometimes the fact you want to authorize on is not in the request — it has to be computed: does this document match a regex? Is this string on a denylist? What is the risk score of this content according to some classifier? An information provider lets you write that computation once, declare its shape, and then reference its result inside a policy as though it were an ordinary context attribute.
A provider is lowering-time sugar over Cedar. When you write a
provider invocation in a policy, Dogwood does not invent a new runtime evaluator.
Instead, at lowering time it hoists the invocation out of the policy
and rewrites it to a reference into context.providers.<id>. The Cedar that the
engine ultimately evaluates contains no provider call at all — just a plain attribute
access and comparison. Then, at authorize time, Dogwood runs the provider’s
declared implementation, and binds its output record into
context.providers.<id> before handing the request to Cedar.
Because providers are hoisted at lowering time, the hoisted field must be typed and
declared in the schema. A rule that calls a provider may use any action scope —
action == Ns::Action::"X", action in [list], action in Group, or a bare
unconstrained action — because the hoisted field is grafted onto every action’s
context and the provider is evaluated for every decision event (see
The provider contract below).
Calling a provider
You call a provider directly inside an ordinary when { … } clause, exactly where
you would write any other Cedar condition. There is no special marker: any
namespace-qualified name (Ns::Fn) is read as a provider invocation. You invoke it, reach into
its output, and compare — all as part of a normal Cedar expression:
permit ( principal, action == Doc::Action::"read", resource )
when {
Strings::Matches(context.input.document, "^[A-Z]+$").matched == true
};
That reads as: call the Strings::Matches provider with the document and a regex,
take the matched field of its output, and require it to be true.
Because the call sits in ordinary Cedar, only the call itself is special — the
projection (.matched) and comparison (== true) are plain Cedar. That means a
provider’s output composes with the full Cedar expression language: arithmetic,
if/then/else, any method, &&/||/!, comparisons against other context
fields — anything Cedar allows.
The shape of a call
A provider call has three parts, in order:
- Invocation —
Ns::Fn(args). The function name must be namespace-qualified: it needs at least two::-separated segments (likeStrings::Matches,Content::Risk,Lists::Blocked). A bare single-segment name is not a valid provider function. More segments are allowed (Ns::Sub::Fn). - Projection — zero or more accessors that reach into the provider’s output
record:
.field(field access) or["key"](index access). Because Cedar has no positional list indexing, an index must be a string key:record["k"]reads thekfield. The projection may be empty, in which case the output is compared directly. - Comparison — any Cedar comparison. Against a
bool/long/stringoutput you use the ordinary operators (==,!=,<,<=,>,>=). Adecimaloutput supports==and!=; to order one, use Cedar’s decimal-extension methods (lessThan,lessThanOrEqual,greaterThan,greaterThanOrEqual) with adecimal("…")literal.
A complete worked example
Here is a provider from the policy author’s side. It uses the Strings::Matches
provider (corpus case 0001_regex_matches_uppercase), which asks whether a document matches a regular
expression. The policy (policy_1.dw):
permit ( principal, action == Doc::Action::"read", resource )
when {
Strings::Matches(context.input.document, "^[A-Z]+$").matched == true
};
Runnable:
examples/provider_regex_matches_uppercase/—dogwood validateanddogwood replay.
Behind it, Strings::Matches is declared in a
providers.json (with argument types [string, string], an output record
{ matched: bool }, and a small Rhai script) — the declaration side is covered in
The provider schema.
What happens: at lowering time, Strings::Matches(context.input.document, "^[A-Z]+$")
is hoisted to a context.providers.<id> reference, and the policy Cedar becomes
context.providers.<id>.matched == true. At authorize time, Dogwood runs the
provider’s evaluate("...the document...", "^[A-Z]+$"), gets back the record
{ matched: … }, binds it into context.providers.<id>, and Cedar evaluates
.matched == true. So "ABC" gives true (permit), "abc" gives false, and
"AB12" gives false.
Combining providers with the rest of a condition
Because a provider call is just part of an ordinary Cedar condition, it combines with
&&, ||, !, parentheses, other provider calls, and plain Cedar terms — the full
expression language (see The policy language).
Two providers combined with && and ! (the Strings::Matches and Lists::Blocked
providers of case 0004_two_providers_and_not) — a fragment; the full rule is the
provider_matches_and_not_blocked bundle:
when {
Strings::Matches(context.input.document, "^[a-z]+$").matched == true
&& !(Lists::Blocked(context.input.document).blocked == true)
};
Disjunction and parentheses (the Lists::Allowed and Strings::Length providers of
case 0007_boolean_or_parens) — a fragment; the full rule is the
provider_allowed_or_short bundle:
when {
(Lists::Allowed(context.input.document).allowed == true
|| Strings::Length(context.input.document).length < 4)
};
Several calls to the same provider, each with plain-Cedar comparisons — case 0005_regex_operations
(a fragment; the full rule is the
provider_regex_analyze_fields bundle):
when {
Regex::Analyze(context.input.document, "^[A-Z]").is_match == true
&& Regex::Analyze(context.input.document, "[0-9]").count >= 3
&& Regex::Analyze(context.input.document, "[0-9]+").first_match == "42"
};
Because the output is plain Cedar, it can feed ordinary Cedar expressions — for
instance, an integer output used in arithmetic alongside an ordinary context
field — case 0010_unwrapped_mixed_with_cedar (a fragment; the full rule is the
provider_int_arithmetic_trusted bundle):
when {
context.input.trusted == true
&& Strings::DigitCount(context.input.document).count + 1 <= 3
};
The comparison, in two forms
The comparison against a provider’s output comes in two flavors, depending on the output’s type.
Operator form uses one of <=, >=, ==, !=, <, >. Verified examples
across the corpus include .matched == true (case 0001_regex_matches_uppercase), .length < 5
(case 0002_length_threshold), and .count >= 2 (case 0008_greater_than_family) — this fragment’s full rule is the
provider_digitcount_operator_ge bundle:
when {
Strings::DigitCount(context.input.document).count >= 2
};
Method form uses Cedar’s decimal comparison methods —
lessThan, lessThanOrEqual, greaterThan, greaterThanOrEqual — to compare a
decimal output against a decimal("…") literal (the Content::Risk provider of
case 0003_decimal_score_method) — a fragment; the full rule is the
provider_risk_decimal_method bundle:
when {
Content::Risk(context.input.document).severityScore.lessThan(decimal("0.5"))
};
Use the method form to order a decimal output; == and != work on a decimal
directly, and the operators cover bool / long / string outputs.
Projection: reaching into the output record
The projection is the path between the invocation and the comparison. Field access
(.field) and index access (["key"]) can be chained. Because Cedar has no
positional list indexing, an index must be a string key: record["k"] reads the
k field.
The Content::Filter provider of case 0006_set_arg_index_projection chains an index accessor and a field
accessor, then compares with the decimal method form (a fragment; the full rule is the
provider_filter_set_index_decimal bundle):
when {
Content::Filter(context.input.document, ["VIOLENCE", "HATE"])["VIOLENCE"].severityScore.lessThan(decimal("0.5"))
};
Here ["VIOLENCE"] selects the VIOLENCE sub-record from the output, .severityScore
reads its field, and .lessThan(decimal("0.5")) compares.
Providers work under permit and forbid
A provider call is independent of the rule’s effect: it works the same under permit
and forbid. Here the Strings::DigitCount provider of case 0008_greater_than_family gates a forbid
(with a catch-all permit alongside):
forbid ( principal, action == Doc::Action::"post", resource )
when {
Strings::DigitCount(context.input.document).count >= 2
};
Runnable:
examples/provider_digitcount_forbid/— theforbidplus a catch-allpermit;dogwood validateanddogwood replay.
A caution on names
A call is read as a provider invocation because of its shape — any
namespace-qualified name — not because it is declared. Declaredness is a separate
check, made at lowering: a namespace-qualified call that matches no declared provider
(and no macro and no Cedar built-in) is a hard error, “unresolved call to Ns::Fn
reached lowering — it is not a declared information provider, not a declared macro,
and not a Cedar built-in”. So a mistyped provider name is caught, not silently
ignored.
Provider arguments
A provider invocation passes arguments positionally. Because a provider is resolved before Cedar runs (it helps build the context Cedar evaluates against), an argument must be a value Dogwood can read off the request event directly. The argument kinds are:
- Attribute-path reference —
context.input.x,principal.id,resource.owner. It begins with one of the rootscontext/principal/resource, followed by one or more.identsegments. At authorize time Dogwood resolves the path against the decision event: acontextpath is looked up on the event’s input (leadingcontextskipped);principal/resourceresolve to the request scope entity, and a trailing.id/.typeprojects that entity’s id or type. A path that does not resolve is null. - String literal —
"…". - Integer literal — an optionally-signed integer (
i64). - Decimal literal —
decimal("0.5")(chiefly a method threshold, e.g.scoreAbove(decimal("0.5"))). - Bool literals —
true/false. - Set —
[ arg, … ], possibly empty, and possibly nesting other args (a set of strings, ints, bools, or nested sets/paths).
Arbitrary Cedar (arithmetic, if/then/else) is not a provider argument —
only the value forms above. This mirrors the temporal-logic argument restriction.
Field-and-string arguments — case 0001_regex_matches_uppercase:
Strings::Matches(context.input.document, "^[A-Z]+$")
A principal-rooted argument — case 0015_principal_id_arg (a fragment; the full rule is the
provider_principal_id_allowlist bundle):
Access::Allowed(principal.id)
A set argument — cases 0006_set_arg_index_projection / 0009_unwrapped_no_marker:
Content::Filter(context.input.document, ["VIOLENCE", "HATE"])
At authorize time, each argument is resolved to a runtime value (event fields and scope entities as above, literals as-is, sets recursively), and the values are passed positionally to the provider’s implementation, matched to its declared argument order.
Declaring a provider. The signature you invoke against — the argument types, the output record, and the implementation that computes it — is declared in a
providers.jsonfile, described in full in The provider schema. This page assumes those declarations already exist and focuses on calling them from a policy.
How a provider binds to context.providers.<id>
Putting the pieces together, here is what happens to a provider invocation from the caller’s point of view.
Lowering time. Every provider invocation is hoisted: the call leaf is replaced
with a reference into context.providers.<id>. The surrounding projection and
comparison were already ordinary Cedar, so they lower natively — an index ["k"]
becomes .k, and the comparison stays as whatever Cedar op you wrote. (The
generated field names and the Cedar-schema augmentation this entails are covered in
The provider schema.)
Authorize time. For each decision event Dogwood builds the Cedar request
context. It passes context.input through from the event, evaluates every
declared provider field (resolving each argument, then running the provider), and
collects the outputs into a single context.providers object keyed by
id. So context.providers.<id> holds that provider’s evaluated output record, and
Cedar evaluates the (already-lowered) comparison against it.
So you write the surface form Ns::Fn(args).field <cmp> literal, and
the engine evaluates context.providers.<id>.field <cmp> literal against the bound
output. For case 0001_regex_matches_uppercase, the engine runs the provider, binds { matched: … }, and
Cedar evaluates .matched == true — giving "ABC" → true, "abc" → false,
"AB12" → false. For case 0006_set_arg_index_projection, document="violent" scores VIOLENCE at 0.90 so
.lessThan(0.5) is false (deny), document="safe" scores 0.10 so it is true
(permit), and document="hateful" scores VIOLENCE at 0.10 (only HATE is 0.90) so it is
also true (permit).
The provider contract
Provider execution is unconditional. A provider invocation belongs to a rule,
but its evaluation is not gated by that rule — not by the rule’s action clause, its
principal/resource constraints, its when/unless conditions, or whether the rule
could fire at all. For every decision event, every provider invocation in the policy set
is evaluated and its output bound into context.providers; Cedar alone then decides which
policies fire, using its ordinary scope and condition semantics. (Deciding “could
this rule match this event?” before running its provider would mean re-implementing
Cedar’s scope semantics inside the provider machinery, so Dogwood does not.)
Three consequences for provider authors:
-
Providers must be pure. A provider may run for events its rule has nothing to do with, and implementations are free to skip, cache, reorder, or repeat evaluations whose results cannot affect the verdicts. A provider must be a deterministic function of its arguments with no observable effects. (Replay —
dogwood replay— and checking one engine against another also assume this.) -
Any argument may be absent. On an event whose context or scope entities do not carry the fields a provider reads (a different action’s input shape, a resource type without the attribute), the argument arrives as Null — in a Rhai script, the unit value
(). Scripts must be defensive: detect unit arguments and return a sentinel that conforms to the declaredoutputTypeinstead of erroring:fn evaluate(text) { if type_of(text) == "()" { return #{ length: -1 }; } #{ length: text.len() } }Choose the sentinel deliberately, and mind the polarity trap: a sentinel that makes a guard false is the restrictive direction under
permit(the rule doesn’t fire) but the permissive direction underforbid(the denial doesn’t fire). Pick the sentinel that fails safe for the polarity of the rules reading it. -
An erroring provider is undefined behavior. If a provider evaluation errors (a script that throws, an external resolver that fails), the decision outcome carries no guarantees. This reference interpreter fails closed: it denies the request and reports the error in the response’s diagnostics. That is an implementation choice, not a contract — other implementations, or future versions of this one, may avoid the error entirely (and reach a different verdict) or handle it differently. A policy set whose safety depends on an erroring provider denying is incorrect on every implementation, including this one. Defensive scripts (point 2) are the only defense.
The same contract applies to external providers supplied through a
ProviderResolver: pure, tolerant of Null arguments, never relying on error
behavior.
How it composes with the rest of a policy
Providers slot into the same rule structure as everything else. A rule scopes a
principal, action, and resource in the usual
Cedar way (see The policy language), and the provider call
lives in an ordinary when { … } clause, freely combined with plain Cedar conditions
and with other providers.
The companion feature is temporal expressions, written with when temporal { … } —
see Temporal expressions. Where a provider computes a
value for the current request, a temporal expression reasons about the history of
requests. Both are lowered into ordinary Cedar the engine can evaluate.
Advanced features
Two provider features exist but most policies do not need them, and both are documented on the declaration-side page, The provider schema:
- The
guardrails { … }clause —when guardrails { E }is transparent sugar for a barewhen { E }; the tag carries no semantics and is retained for compatibility with existing policies. You can call a provider from an ordinarywhenjust as well. - Output methods and no-implementation providers — post-processing a provider’s
output with a declared method (
Provider::Fn(args).method(…)), and declaring a provider with no implementation so its value is supplied by your own code.
See The provider schema for both.
See also
- The provider schema — declaring a provider: the
providers.jsonformat, the Rhai implementation contract, and the advanced features above. - The policy language — rule structure and the Cedar condition language a provider call lives in.
- Temporal expressions — the companion feature, reasoning about request history.
- Calling macros — the other way to reuse logic across policies.
- The API and workflow — wiring provider declarations
into a
ServiceSchema(ServiceSchemaBuilder::providers) and the authorize-time flow that evaluates providers.