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

Policy Evaluation

When a user attempts to log in to a host, the policy engine needs to decide: allow or deny? This page explains how that decision is made and which evaluation methods to use.

How evaluation works

The engine processes rules in four steps:

  1. Filter: Only enabled rules participate. Disabled rules are skipped entirely.
  2. Match: Each remaining rule is checked against all three dimensions of the request (user, host, service). A rule matches only if all three dimensions match.
  3. Deny precedence: If any matching rule is a deny rule, the request is denied regardless of how many allow rules also match.
  4. Default deny: If no allow rule matches (and no deny rule matched), the request is denied. HBAC is deny-by-default.

This means that allow rules grant access and deny rules revoke it, with denials always taking priority.

Dimension matching

For each dimension, a rule matches if any of these conditions holds:

  • The dimension is set to category=all.
  • The request’s entity name appears in the rule’s names set.
  • Any of the request’s groups overlap with the rule’s groups set.

For the user dimension, the entity name is Subject.id and the groups are Subject.roles. For host and service dimensions, the names and groups come from HbacRequest.

Evaluation result

Every evaluation method returns an HbacEvaluationResult:

#![allow(unused)]
fn main() {
pub struct HbacEvaluationResult {
    pub allowed: bool,
    pub matched_rules: Vec<String>,
    pub not_matched_rules: Vec<String>,
}
}
  • allowed – the final decision.
  • matched_rules – names of rules that matched all three dimensions.
  • not_matched_rules – names of enabled rules that did not match.

The is_allowed() and is_denied() methods provide convenient boolean checks.

Evaluation methods

check_access: fast boolean

When you only need a yes/no answer and do not care which rules matched:

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

let allowed: bool = policy.check_access(&request);
}

This is the fastest path – it short-circuits as soon as a decision can be made.

evaluate: standard evaluation

Returns the full result with the list of matched and unmatched rules:

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

let result = policy.evaluate(&request);
if result.is_allowed() {
    println!("Access granted by: {:?}", result.matched_rules);
} else {
    println!("Access denied. Rules checked: {:?}", result.not_matched_rules);
}
}

evaluate_detailed: debugging

Identical to evaluate but populates not_matched_rules with the names of rules that were checked but did not match. Use this when debugging unexpected denials:

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

let result = policy.evaluate_detailed(&request);
if result.is_denied() {
    println!("Denied. These rules did not match:");
    for name in &result.not_matched_rules {
        println!("  - {}", name);
    }
}
}

evaluate_at: temporal evaluation

Evaluates the request at a specific timestamp. Temporal rules whose validity window does not include the timestamp are excluded:

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

let now = current_timestamp_millis();
let result = policy.evaluate_at(&request, now);
}

There is also evaluate_detailed_at for the detailed variant.

evaluate_and_apply: grant permissions

Evaluates the request and, if access is allowed, applies the corresponding permissions to the request’s subject. This integrates HBAC decisions with acls-rs permission tracking:

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

let alice = Subject::new("alice");
let mut request = HbacRequest::new(alice, "server.example.com", "sshd");

let (result, permissions) = policy.evaluate_and_apply(&mut request);

if result.is_allowed() {
    // request.user now carries the granted AtomicPermission
    for perm in &permissions {
        println!("Granted: {}", perm);
    }
}
}

A temporal variant, evaluate_and_apply_at, accepts a timestamp.

Debugging unexpected denials

When a user reports “I can’t log in”, work through this checklist:

  1. Is the rule enabled? Check rule.enabled.
  2. Does the user dimension match? Is the user’s ID or any of their roles in the rule’s user set? Or is the user dimension set to category=all?
  3. Does the host dimension match? Is the request’s target host or any of its groups in the rule’s host set?
  4. Does the service dimension match? Same check for services.
  5. Is a deny rule matching? A matching deny rule overrides all allow rules.
  6. Is the rule temporal? Check whether the current time falls within the rule’s validity window.

Use evaluate_detailed to see which rules were checked and did not match.

Performance considerations

Without caching, evaluation is O(n) where n is the number of enabled rules. The optimization stack (described in Caching System) reduces this to sub-microsecond cached lookups for large rule sets.

See Performance Results for benchmark data at different rule counts.

Next steps

As your rule count grows, linear evaluation becomes a bottleneck. The next page introduces the caching pipeline that brings evaluation time down to sub-microsecond latency.

See Caching System.