ADR-0009: Closed-world per-field rules: the rule is the catalog¶
- Status: accepted
- Date: 2026-08-24 (updated 2026-11-02: plain-Rego rules bound by regime and service)
- Satisfies: P-02, D-11
- Supersedes: ADR-0008
- Deliberation: engine.rego (design notes in comments), REQ-48, REQ-97, F-90, demo commit
80af776(rules as plain Rego), PR #527 (service binding) - Implemented in: policies/gbo: engine.rego (regime and service binding), lib.rego (reads the request only), rules/, engine_test.rego (every rule names its services)
Context and problem statement¶
For each requested field, the PDP must decide whether an authorization basis covers the field. The PDP must do this without a separate catalog, because a catalog can drift away from enforcement.
A field key (ParentType.field) is unique only within one schema. Two
services can share a key: Query.ingeschrevenPersoon is in both bri and
brp (F-90).
Decision¶
Each rule is plain Rego. A rule declares these items:
rule_id,regime(consentorpid), andservices(the FSC services that the rule applies to)covers_fieldsandcovers_types: the fields that the rule can grant- its reasons to refuse:
denyfor the whole request anddeny_field[i]for one field
If a rule gives no reason, it grants the field. Each rule checks the actor
and the scope itself (REQ-48).
lib.rego only reads the request. It holds no checks.
The engine binds rules to fields in four steps:
- Only the rules of the active regime bind (ADR-0031).
- Only the rules that serve the FSC service of the request bind. The
service is the
service_namefrom the FSC access token. The Manager of the source signed this token (REQ-97). - A field binds to the rules that name its key. An argument-free leaf outside the root types also binds to the rules that cover its parent type.
- For each field, the first bound rule without reasons grants the field.
The rule set is a closed world:
- If no rule covers a field, the PDP denies the field with
NO_APPLICABLE_RULE. - If the PDP cannot make the decision for a field, it refuses the field
as
RULE_NOT_EVALUABLE. The request then fails as a configuration error. - If the field list of a request is uncertain, the PDP denies the whole request (REQ-17, ADR-0013).
The demo has five rules, as the following table shows:
| Rule | Regime | Service | Grants |
|---|---|---|---|
| DVT0001 | consent | bri |
Income declaration fields under consent |
| LVG0001 | consent | lvg |
Ownership check at the LVG, scope lvg:vbo:eigendom |
| EUD0001 | pid | bri |
Income statement (inkomensverklaring) to the wallet, years 2024 and 2025 |
| EUD0002 | pid | brp |
Death certificate (akte van overlijden) to the wallet |
| ASI0001 | pid | brp |
BRP fields for the ASIP |
Consequences¶
- One artifact defines what is allowed and why. A new access decision needs a new rule file with tests.
- A test requires every rule to name its services. A rule for one service cannot grant a field of another service with the same key.
- Consent per year works because each scope covers one year
(
bd:ib:2025). Also, the year selector must travel inside the query. If the filter is missing, the request fails closed. -
Reasons have two layers (REQ-06):
- The consumer and the ADL get only a client code, such as
FIELD_NOT_PERMITTED. - The rule codes for each field go only to the console decision log for the operator, in a fixed order.
For more information, see Authorization.
- The consumer and the ADL get only a client code, such as
-
The service catalog (dienstencatalogus) with a legal anchor from ADR-0008 stays an open governance question. Risks and technical debt tracks it.
- gbo-demo v0.9.0 has no test that makes sure that every rule checks the actor itself (#604).