ABAC Policies
Your platform’s security team mandates a new policy: database write access is only allowed from the corporate network during business hours. Roles cannot express this – a “database-writer” role either grants the permission or it does not, regardless of where or when the request originates.
Attribute-Based Access Control (ABAC) solves this by making access decisions based on runtime attributes: who the user is, what they are trying to access, and the environment in which the request occurs.
Attribute context
An AttributeContext is a HashMap<String, String> that carries the runtime
attributes available at decision time. You populate it from whatever sources
your application has – request headers, environment variables, identity
provider claims:
#![allow(unused)]
fn main() {
use std::collections::HashMap;
use acls_rs::policy::AttributeContext;
let mut context = AttributeContext::new();
context.insert("department".into(), "engineering".into());
context.insert("network".into(), "corporate".into());
context.insert("time_of_day".into(), "business-hours".into());
context.insert("clearance".into(), "standard".into());
}
Attribute rules
An AttributeRule maps a set of required attributes to permissions that are
granted (or denied) when those attributes are present and match. The rule
checks whether every key-value pair in the rule’s attribute map appears in the
context.
#![allow(unused)]
fn main() {
use std::collections::HashMap;
use acls_rs::prelude::*;
// Grant database:write when network=corporate AND time_of_day=business-hours
let rule = AttributeRule::new()
.grant(
AtomicPermission::new("database", "write"),
HashMap::from([
("network".into(), "corporate".into()),
("time_of_day".into(), "business-hours".into()),
]),
);
}
Rules can also carry denials. A denial matches when its attribute requirements
are met and overrides any corresponding grant, just like GrantDenialPair:
#![allow(unused)]
fn main() {
use std::collections::HashMap;
use acls_rs::prelude::*;
// Deny production:deploy when environment=staging
let rule = AttributeRule::new()
.deny(
AtomicPermission::new("production", "deploy"),
HashMap::from([
("environment".into(), "staging".into()),
]),
);
}
You can chain multiple grants and denials on a single rule:
#![allow(unused)]
fn main() {
use std::collections::HashMap;
use acls_rs::prelude::*;
let rule = AttributeRule::new()
.grant(
AtomicPermission::new("api", "read"),
HashMap::from([("authenticated".into(), "true".into())]),
)
.grant(
AtomicPermission::new("api", "write"),
HashMap::from([
("authenticated".into(), "true".into()),
("role".into(), "editor".into()),
]),
)
.deny(
AtomicPermission::new("api", "admin"),
HashMap::from([("role".into(), "guest".into())]),
);
}
Building a policy
An AbacPolicy collects rules and evaluates them against a context. By
default, when multiple rules produce conflicting results, the policy uses
MeetResolver (intersection – the most restrictive outcome wins):
#![allow(unused)]
fn main() {
use std::collections::HashMap;
use acls_rs::prelude::*;
let corp_db_rule = AttributeRule::new()
.grant(
AtomicPermission::new("database", "write"),
HashMap::from([
("network".into(), "corporate".into()),
("time_of_day".into(), "business-hours".into()),
]),
);
let read_anywhere = AttributeRule::new()
.grant(
AtomicPermission::new("database", "read"),
HashMap::from([("authenticated".into(), "true".into())]),
);
let mut policy = AbacPolicy::new();
policy.add_rule(corp_db_rule);
policy.add_rule(read_anywhere);
// Context: corporate network, business hours, authenticated
let mut context: AttributeContext = HashMap::new();
context.insert("network".into(), "corporate".into());
context.insert("time_of_day".into(), "business-hours".into());
context.insert("authenticated".into(), "true".into());
let result = policy.evaluate(&context);
assert!(result.has_permission(&AtomicPermission::new("database", "write")));
assert!(result.has_permission(&AtomicPermission::new("database", "read")));
// Context: home network -- database:write is not granted
let mut home_context: AttributeContext = HashMap::new();
home_context.insert("network".into(), "home".into());
home_context.insert("authenticated".into(), "true".into());
let result = policy.evaluate(&home_context);
assert!(!result.has_permission(&AtomicPermission::new("database", "write")));
assert!(result.has_permission(&AtomicPermission::new("database", "read")));
}
Conflict resolution
When multiple rules grant and deny the same permission, the conflict resolver determines the outcome. Three resolvers are available:
MeetResolver(default): intersection of all rule results. The most restrictive interpretation wins. Use this for high-security environments.JoinResolver: union of all rule results. The most permissive interpretation wins. Use this for convenience-first environments.PriorityResolver: assigns a priority to each rule and lets the highest-priority rule(s) win, falling back toMeetResolverto break ties between rules that share the top priority.
#![allow(unused)]
fn main() {
use acls_rs::prelude::*;
// Use JoinResolver for a more permissive policy
let policy = AbacPolicy::with_resolver(JoinResolver);
}
PriorityResolver does not implement ConflictResolver, so it cannot be
passed to AbacPolicy::with_resolver. Instead, call resolve_with_ids
directly with the rule results tagged by ID:
#![allow(unused)]
fn main() {
use acls_rs::prelude::*;
let mut resolver = PriorityResolver::new();
resolver.set_priority(0, 10); // admin rule: higher priority
resolver.set_priority(1, 5); // safety-lock rule: lower priority
let admin_rule = GrantDenialPair::new(
PermissionSet::from([AtomicPermission::new("file", "delete")]),
PermissionSet::new(),
);
let safety_rule = GrantDenialPair::new(
PermissionSet::new(),
PermissionSet::from([AtomicPermission::new("file", "delete")]),
);
// Rule 0 (priority 10) wins over rule 1 (priority 5)
let resolved = resolver.resolve_with_ids(vec![(0, admin_rule), (1, safety_rule)]);
assert!(resolved.has_permission(&AtomicPermission::new("file", "delete")));
}
Combining ABAC with RBAC
ABAC and RBAC address different concerns. A practical pattern is to use RBAC for static role assignments and ABAC for dynamic refinement:
#![allow(unused)]
fn main() {
use std::collections::HashMap;
use acls_rs::prelude::*;
// RBAC: resolve base permissions from roles
let mut rbac = RbacPolicy::new();
// ... add roles ...
let roles = vec!["developer".to_string()];
let role_perms = rbac.effective_permissions(&roles).unwrap();
// ABAC: evaluate context-dependent permissions
let mut abac = AbacPolicy::new();
// ... add rules ...
let context: AttributeContext = HashMap::new();
// ... populate context ...
let context_perms = abac.evaluate(&context).effective_permissions();
// Intersect: user gets only what both RBAC and ABAC agree on
let effective = role_perms.meet(context_perms);
}
Next steps
ABAC handles context at decision time, but it does not handle time itself. Your platform hires contractors who need access for exactly three months, and on-call engineers who need emergency admin access for 24 hours. The next chapter introduces temporal permissions – access that activates and expires automatically.
See Temporal Permissions.