use actr_protocol::{Acl, AclRule, ActrId};
pub fn check_acl_permission(
caller_id: Option<&ActrId>,
target_id: &ActrId,
acl: Option<&Acl>,
) -> Result<bool, String> {
if caller_id.is_none() {
tracing::trace!("ACL: local call, allowing");
return Ok(true);
}
let caller = caller_id.unwrap();
let acl = match acl {
Some(a) => a,
None => {
tracing::trace!(
"ACL: no ACL configured, allowing {} -> {}",
caller,
target_id,
);
return Ok(true);
}
};
if acl.rules.is_empty() {
tracing::warn!(
"ACL: empty rule set, denying {} -> {} (default deny)",
caller,
target_id,
);
return Ok(false);
}
let mut any_allow = false;
for rule in &acl.rules {
if !matches_rule(caller, rule) {
continue;
}
let is_allow = rule.permission == actr_protocol::acl_rule::Permission::Allow as i32;
if !is_allow {
tracing::debug!("ACL: DENY rule matched for {} -> {}", caller, target_id,);
return Ok(false);
}
any_allow = true;
}
if any_allow {
tracing::debug!("ACL: ALLOW rule matched for {} -> {}", caller, target_id,);
return Ok(true);
}
tracing::warn!(
"ACL: no matching rule, denying {} -> {} (default deny)",
caller,
target_id,
);
Ok(false)
}
fn matches_rule(caller: &ActrId, rule: &AclRule) -> bool {
use actr_protocol::acl_rule::SourceRealm;
if caller.r#type != rule.from_type {
return false;
}
match &rule.source_realm {
None | Some(SourceRealm::AnyRealm(_)) => true,
Some(SourceRealm::RealmId(id)) => caller.realm.realm_id == *id,
}
}
#[cfg(test)]
mod tests {
use super::*;
use actr_protocol::{ActrType, Realm, acl_rule::Permission, acl_rule::SourceRealm};
fn make_id(manufacturer: &str, name: &str, version: &str, realm_id: u32) -> ActrId {
ActrId {
serial_number: 0xaabb,
r#type: ActrType {
manufacturer: manufacturer.into(),
name: name.into(),
version: version.into(),
},
realm: Realm { realm_id },
}
}
fn make_rule(manufacturer: &str, name: &str, version: &str, perm: Permission) -> AclRule {
AclRule {
permission: perm as i32,
from_type: ActrType {
manufacturer: manufacturer.into(),
name: name.into(),
version: version.into(),
},
source_realm: None,
}
}
#[test]
fn local_call_always_allowed() {
let target = make_id("acme", "svc", "0.1.0", 1);
assert!(check_acl_permission(None, &target, None).unwrap());
}
#[test]
fn no_acl_allows_by_default() {
let caller = make_id("acme", "client", "0.1.0", 1);
let target = make_id("acme", "svc", "0.1.0", 1);
assert!(check_acl_permission(Some(&caller), &target, None).unwrap());
}
#[test]
fn empty_rules_denies() {
let caller = make_id("acme", "client", "0.1.0", 1);
let target = make_id("acme", "svc", "0.1.0", 1);
let acl = Acl { rules: vec![] };
assert!(!check_acl_permission(Some(&caller), &target, Some(&acl)).unwrap());
}
#[test]
fn deny_overrides_allow() {
let caller = make_id("acme", "client", "0.1.0", 1);
let target = make_id("acme", "svc", "0.1.0", 1);
let acl = Acl {
rules: vec![
make_rule("acme", "client", "0.1.0", Permission::Allow),
make_rule("acme", "client", "0.1.0", Permission::Deny),
],
};
assert!(!check_acl_permission(Some(&caller), &target, Some(&acl)).unwrap());
}
#[test]
fn allow_when_matched() {
let caller = make_id("acme", "client", "0.1.0", 1);
let target = make_id("acme", "svc", "0.1.0", 1);
let acl = Acl {
rules: vec![make_rule("acme", "client", "0.1.0", Permission::Allow)],
};
assert!(check_acl_permission(Some(&caller), &target, Some(&acl)).unwrap());
}
#[test]
fn no_match_denies() {
let caller = make_id("acme", "client", "0.1.0", 1);
let target = make_id("acme", "svc", "0.1.0", 1);
let acl = Acl {
rules: vec![make_rule("other", "other", "0.1.0", Permission::Allow)],
};
assert!(!check_acl_permission(Some(&caller), &target, Some(&acl)).unwrap());
}
#[test]
fn any_realm_rule_matches_foreign_realm() {
let caller = make_id("acme", "client", "0.1.0", 2002);
let target = make_id("acme", "svc", "0.1.0", 1001);
let acl = Acl {
rules: vec![AclRule {
permission: Permission::Allow as i32,
from_type: ActrType {
manufacturer: "acme".into(),
name: "client".into(),
version: "0.1.0".into(),
},
source_realm: Some(SourceRealm::AnyRealm(true)),
}],
};
assert!(check_acl_permission(Some(&caller), &target, Some(&acl)).unwrap());
}
}