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

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. Two 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.
#![allow(unused)]
fn main() {
use acls_rs::prelude::*;

// Use JoinResolver for a more permissive policy
let policy = AbacPolicy::with_resolver(JoinResolver);
}

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.