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

HBAC Rules

An HBAC rule answers one question: for a given combination of user, host, and service, should access be allowed or denied? This page covers how to construct rules for the access patterns your organization needs.

Creating a rule

Every rule starts with a name and is disabled by default. You populate the three dimensions – users, hosts, and services – and then enable the rule:

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

let rule = HbacRule::builder("allow_ssh_devs")
    .user_group("developers")
    .host_group("dev-servers")
    .service("sshd")
    .enabled(true)
    .build()
    .unwrap();
}

The add_* methods return Result<(), CategoryError>. They fail only if you try to add a specific entity to a dimension that is already set to category=all – the two modes are mutually exclusive.

Category=all vs. specific entities

Each dimension is either “all” (matches everything) or a list of specific names and groups. Use category=all when the rule should not restrict that dimension:

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

// Allow any user to SSH into the bastion host
let bastion_rule = HbacRule::builder("bastion_ssh")
    .user_category_all()                              // any user
    .host("bastion.example.com")                      // this host only
    .service("sshd")                                  // SSH only
    .enabled(true)
    .build()
    .unwrap();
}

Setting category=all and then calling add_user() on the same dimension returns Err(CategoryError::CannotModifyAll).

Groups

Users, hosts, and services can be added individually or by group. Groups are resolved via the Subject’s roles (for users) and the request’s group fields (for hosts and services):

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

let rule = HbacRule::builder("dba_postgres")
    .user_group("dba-team")
    .host_group("database-servers")
    .service("postgresql")
    .service("pgbouncer")
    .enabled(true)
    .build()
    .unwrap();

// A user matches if their ID or any role matches the rule's user set
let mut dba = Subject::new("bob");
dba.roles.push("dba-team".to_string());

let request = HbacRequest::builder()
    .user(dba)
    .targethost("db01.example.com")
    .targethost_group("database-servers")
    .service("postgresql")
    .build()
    .unwrap();
}

The HbacRequest::builder() lets you attach host groups and service groups that the matching engine checks alongside direct names.

Deny rules

By default, rules allow access. A deny rule blocks access even when other rules would allow it – denials take precedence:

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

let mut policy = HbacPolicy::new();

// Allow all developers to access dev servers
let allow = HbacRule::builder("allow_devs")
    .user_group("developers")
    .host_group("dev-servers")
    .service_category_all()
    .enabled(true)
    .build()
    .unwrap();
policy.add_rule(allow);

// Deny everyone from production hosts
let deny = HbacRule::builder("deny_production")
    .deny()
    .user_category_all()
    .host_group("production")
    .service_category_all()
    .enabled(true)
    .build()
    .unwrap();
policy.add_rule(deny);

// A developer requesting access to a production host is denied,
// even though the allow rule matches their group
let dev = Subject::new("alice");
let request = HbacRequest::new(dev, "prod-web-01.example.com", "sshd");
let result = policy.evaluate(&request);
assert!(result.is_denied());
}

Use HbacRule::builder(name).deny() to create deny rules. You can check a rule’s type with rule.is_allow() and rule.is_deny().

Enable and disable

Rules participate in evaluation only when enabled. This lets you prepare rules in advance and activate them on schedule:

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

let mut rule = HbacRule::builder("weekend_maintenance")
    // ... configure dimensions ...
    .build()
    .unwrap();

// Disabled by default -- does not affect evaluation
assert!(!rule.enabled);

rule.enable();
// Now participates in evaluation

rule.disable();
// Temporarily removed from evaluation without deleting the rule
}

You can also enable or disable rules through the policy:

#![allow(unused)]
fn main() {
policy.enable_rule("weekend_maintenance");
policy.disable_rule("weekend_maintenance");
}

HbacResource: permission-based modeling

HbacResource models a (host, service) pair as an acls-rs Resource with permissions. This is useful when you want to check access via the permission system rather than rule evaluation:

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

let resource = HbacResource::new("database.example.com", "postgresql");
let mut alice = Subject::new("alice");

// Alice has no permissions yet
assert!(!resource.can_access(&alice));

// Grant access (adds the corresponding AtomicPermission to Alice)
resource.grant_access(&mut alice);
assert!(resource.can_access(&alice));

// Check what permission is required
let required = resource.required_permission();
assert!(alice.has_permission(&required));

// Revoke access
resource.deny_access(&mut alice);
assert!(!resource.can_access(&alice));
}

The builder provides more control over groups:

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

let resource = HbacResource::builder()
    .host("db01.example.com")
    .service("postgresql")
    .host_group("database-servers")
    .service_group("databases")
    .build()
    .unwrap();
}

Evaluate and apply

When a policy grants access, you often want to record that decision as a permission on the subject. evaluate_and_apply does both in one call:

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

let mut policy = HbacPolicy::new();
// ... add rules ...

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
    assert!(!permissions.is_empty());
}
}

Putting it together

A typical server setup with layered rules:

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

let mut policy = HbacPolicy::new();

// Baseline: SSH access for all employees to all hosts
let ssh_all = HbacRule::builder("employee_ssh")
    .user_category_all()
    .host_category_all()
    .service("sshd")
    .enabled(true)
    .build()
    .unwrap();
policy.add_rule(ssh_all);

// Specific: DBAs can access PostgreSQL on database servers
let dba_pg = HbacRule::builder("dba_postgres")
    .user_group("dba-team")
    .host_group("database-servers")
    .service("postgresql")
    .enabled(true)
    .build()
    .unwrap();
policy.add_rule(dba_pg);

// Lockdown: deny all access to production hosts
let deny_prod = HbacRule::builder("deny_production")
    .deny()
    .user_category_all()
    .host_group("production")
    .service_category_all()
    .enabled(true)
    .build()
    .unwrap();
policy.add_rule(deny_prod);

// Test: DBA can access dev database
let mut dba = Subject::new("bob");
dba.roles.push("dba-team".to_string());

let dev_request = HbacRequest::builder()
    .user(dba.clone())
    .targethost("db-dev-01.example.com")
    .targethost_group("database-servers")
    .service("postgresql")
    .build()
    .unwrap();

assert!(policy.evaluate(&dev_request).is_allowed());

// Test: DBA cannot access production database (deny rule wins)
let prod_request = HbacRequest::builder()
    .user(dba)
    .targethost("db-prod-01.example.com")
    .targethost_group("production")
    .service("postgresql")
    .build()
    .unwrap();

assert!(policy.evaluate(&prod_request).is_denied());
}

Next steps

You know how to build rules. The next page explains how the evaluation engine processes them – the matching logic, deny precedence, and the different evaluation methods available.

See Policy Evaluation.