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:
- Filter: Only enabled rules participate. Disabled rules are skipped entirely.
- 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.
- Deny precedence: If any matching rule is a deny rule, the request is denied regardless of how many allow rules also match.
- 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:
- Is the rule enabled? Check
rule.enabled. - 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?
- Does the host dimension match? Is the request’s target host or any of its groups in the rule’s host set?
- Does the service dimension match? Same check for services.
- Is a deny rule matching? A matching deny rule overrides all allow rules.
- 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.