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

Rules and Policies

Validation only checks the shape of a request. Your bank’s policy is about its contents: payment_initiation details must use EUR, must never exceed 10,000.00 in a single instructed amount, and must request the initiate action. The authorization_details object is the attribute set such a policy speaks about.

authz-details-rs packages such policies as named AuthzDetailsRules held in an AuthzDetailsPolicy, the same way hbac-rs packages HbacRules in an HbacPolicy.

Building a rule

A rule has a unique name, the type it applies to, and conditions combined with a MatchMode. The builder validates the rule (type present, JSONPaths well-formed, times parseable) when you call build():

#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;

let rule = AuthzDetailsRule::builder("eur-payments-up-to-10000")
    .type_id("payment_initiation")
    .field("$.instructedAmount.currency").eq("EUR")
    .field("$.instructedAmount.amount").le(10_000)
    .field("$.actions").contains("initiate")
    .build()
    .unwrap();

assert_eq!(rule.conditions.len(), 3);
}

field(path) starts a condition and an operator method finishes it: eq, ne, lt, le, gt, ge, is_in, not_in, contains, op (any ComparisonOp), and matches with the regex-matchers feature. Time conditions are added with during, during_on, on_days, within and outside (see Temporal Rules).

MatchMode::All (the default) requires every condition to hold; .any() switches to MatchMode::Any, the right mode for “the request may satisfy any one of these”:

#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;

let small_or_usd = AuthzDetailsRule::builder("small-or-usd")
    .type_id("payment_initiation")
    .any()
    .field("$.instructedAmount.amount").le(100)
    .field("$.instructedAmount.currency").eq("USD")
    .build()
    .unwrap();
}

Rules are plain data: they serialize with serde, so a policy can be loaded from configuration and shared between the authorization server and the resource servers.

Evaluating details with a policy

Every requested detail must pass at least one enabled rule of its type. The result says which rules matched, so decisions can be audited:

#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;
use serde_json::json;

let mut policy = AuthzDetailsPolicy::new();
for (name, currency) in [("eur-only", "EUR"), ("usd-only", "USD")] {
    policy
        .add_rule(
            AuthzDetailsRule::builder(name)
                .type_id("payment_initiation")
                .field("$.instructedAmount.currency").eq(currency)
                .build()
                .unwrap(),
        )
        .unwrap();
}

let eur = [AuthorizationDetail::new(
    "payment_initiation",
    json!({"actions": ["initiate"], "instructedAmount": {"currency": "EUR", "amount": "250.00"}}),
)];
let result = policy.evaluate(&eur);
assert!(result.is_allowed());
assert_eq!(result.verdict, Verdict::Allow);
assert_eq!(result.matched_rules, ["eur-only"]);
assert_eq!(result.not_matched_rules, ["usd-only"]);

// A GBP detail passes neither rule
let gbp = [AuthorizationDetail::new(
    "payment_initiation",
    json!({"actions": ["initiate"], "instructedAmount": {"currency": "GBP", "amount": "250.00"}}),
)];
let result = policy.evaluate(&gbp);
assert!(result.is_denied());
assert_eq!(result.verdict, Verdict::Deny);
assert!(result.has_warnings());
}
VerdictWhen
Verdict::AllowEvery detail passed at least one rule of its type
Verdict::DenySome detail passed no rule – including a detail whose type has no rule, once any rule applies to the request
Verdict::NoRuleNo rule applies to any requested type (or the request is empty)

NoRule is what lets composed policies fall back to another dimension; on its own it is not an allow.

evaluate uses the current time; evaluate_at(details, timestamp) pins it (an acls-rs Timestamp, milliseconds since the Unix epoch), and check_access / check_access_at return just the boolean. The policy accepts anything that is AsRef<[AuthorizationDetail]>: a slice, a Vec, AuthorizationDetails, or an AuthzDetailsRequest.

Managing rules

Policies mirror HbacPolicy: rules are addressed by name.

#![allow(unused)]
fn main() {
use authz_details_rs::prelude::*;

let rule = |name: &str, cap: u32| {
    AuthzDetailsRule::builder(name)
        .type_id("payment_initiation")
        .field("$.instructedAmount.amount").le(cap)
        .build()
        .unwrap()
};

let mut policy = AuthzDetailsPolicy::with_max_rules(2);
policy.add_rule(rule("small", 100)).unwrap();
assert!(matches!(
    policy.add_rule(rule("small", 200)),
    Err(PolicyError::DuplicateRule { .. })
));
policy.add_rule(rule("large", 10_000)).unwrap();
assert!(matches!(
    policy.add_rule(rule("huge", 1_000_000)),
    Err(PolicyError::TooManyRules { .. })
));

assert!(policy.disable_rule("large"));
assert!(policy.enable_rule("large"));
assert!(policy.remove_rule("large"));
assert_eq!(policy.len(), 1);
}

add_rule rejects malformed rules (PolicyError::InvalidRule), duplicate names, and additions beyond with_max_rules (a DoS guard shared with the other BAC policy cores). load_rules replaces the permanent rules atomically.

Error handling

Policy outcomes are values; errors signal malformed input:

ErrorRaised byMeaning
RuleError::MissingTypeIdbuild()The rule does not name a type
RuleError::InvalidPathbuild()A JSONPath is malformed or deeper than 10 segments
RuleError::InvalidTimeOfDaybuild()A time is not HH:MM or HH:MM:SS
PolicyError::InvalidRule / DuplicateRule / TooManyRulesadd_ruleSee above
ConstraintViolation::TypeMismatchrule.evaluateA comparison met an unexpected value type; the policy records it as a warning and the rule fails

Best practices

  • Name rules after their intent (eur-payments-up-to-10000), so matched_rules reads as an audit trail.
  • Keep independent concerns in separate rules so they can be disabled and revoked independently.
  • Evaluate the same policy at the authorization server (before granting) and at the resource server (before acting); rules serialize cleanly between services.

Next steps

Pin rules to validity windows and business hours with Temporal Rules, compose them with RBAC in the acls-rs Bridge and with host access in HBAC Integration.