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.