1use actr_protocol::{Acl, AclRule, ActrId};
9
10pub fn check_acl_permission(
25 caller_id: Option<&ActrId>,
26 target_id: &ActrId,
27 acl: Option<&Acl>,
28) -> Result<bool, String> {
29 if caller_id.is_none() {
31 tracing::trace!("ACL: local call, allowing");
32 return Ok(true);
33 }
34
35 let caller = caller_id.unwrap();
36
37 let acl = match acl {
39 Some(a) => a,
40 None => {
41 tracing::trace!(
42 "ACL: no ACL configured, allowing {} -> {}",
43 caller,
44 target_id,
45 );
46 return Ok(true);
47 }
48 };
49
50 if acl.rules.is_empty() {
52 tracing::warn!(
53 "ACL: empty rule set, denying {} -> {} (default deny)",
54 caller,
55 target_id,
56 );
57 return Ok(false);
58 }
59
60 let mut any_allow = false;
62 for rule in &acl.rules {
63 if !matches_rule(caller, rule) {
64 continue;
65 }
66 let is_allow = rule.permission == actr_protocol::acl_rule::Permission::Allow as i32;
67 if !is_allow {
68 tracing::debug!("ACL: DENY rule matched for {} -> {}", caller, target_id,);
69 return Ok(false);
70 }
71 any_allow = true;
72 }
73
74 if any_allow {
75 tracing::debug!("ACL: ALLOW rule matched for {} -> {}", caller, target_id,);
76 return Ok(true);
77 }
78
79 tracing::warn!(
81 "ACL: no matching rule, denying {} -> {} (default deny)",
82 caller,
83 target_id,
84 );
85 Ok(false)
86}
87
88fn matches_rule(caller: &ActrId, rule: &AclRule) -> bool {
90 use actr_protocol::acl_rule::SourceRealm;
91
92 if caller.r#type != rule.from_type {
94 return false;
95 }
96
97 match &rule.source_realm {
99 None | Some(SourceRealm::AnyRealm(_)) => true,
100 Some(SourceRealm::RealmId(id)) => caller.realm.realm_id == *id,
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use actr_protocol::{ActrType, Realm, acl_rule::Permission, acl_rule::SourceRealm};
108
109 fn make_id(manufacturer: &str, name: &str, version: &str, realm_id: u32) -> ActrId {
110 ActrId {
111 serial_number: 0xaabb,
112 r#type: ActrType {
113 manufacturer: manufacturer.into(),
114 name: name.into(),
115 version: version.into(),
116 },
117 realm: Realm { realm_id },
118 }
119 }
120
121 fn make_rule(manufacturer: &str, name: &str, version: &str, perm: Permission) -> AclRule {
122 AclRule {
123 permission: perm as i32,
124 from_type: ActrType {
125 manufacturer: manufacturer.into(),
126 name: name.into(),
127 version: version.into(),
128 },
129 source_realm: None,
130 }
131 }
132
133 #[test]
134 fn local_call_always_allowed() {
135 let target = make_id("acme", "svc", "0.1.0", 1);
136 assert!(check_acl_permission(None, &target, None).unwrap());
137 }
138
139 #[test]
140 fn no_acl_allows_by_default() {
141 let caller = make_id("acme", "client", "0.1.0", 1);
142 let target = make_id("acme", "svc", "0.1.0", 1);
143 assert!(check_acl_permission(Some(&caller), &target, None).unwrap());
144 }
145
146 #[test]
147 fn empty_rules_denies() {
148 let caller = make_id("acme", "client", "0.1.0", 1);
149 let target = make_id("acme", "svc", "0.1.0", 1);
150 let acl = Acl { rules: vec![] };
151 assert!(!check_acl_permission(Some(&caller), &target, Some(&acl)).unwrap());
152 }
153
154 #[test]
155 fn deny_overrides_allow() {
156 let caller = make_id("acme", "client", "0.1.0", 1);
157 let target = make_id("acme", "svc", "0.1.0", 1);
158 let acl = Acl {
159 rules: vec![
160 make_rule("acme", "client", "0.1.0", Permission::Allow),
161 make_rule("acme", "client", "0.1.0", Permission::Deny),
162 ],
163 };
164 assert!(!check_acl_permission(Some(&caller), &target, Some(&acl)).unwrap());
165 }
166
167 #[test]
168 fn allow_when_matched() {
169 let caller = make_id("acme", "client", "0.1.0", 1);
170 let target = make_id("acme", "svc", "0.1.0", 1);
171 let acl = Acl {
172 rules: vec![make_rule("acme", "client", "0.1.0", Permission::Allow)],
173 };
174 assert!(check_acl_permission(Some(&caller), &target, Some(&acl)).unwrap());
175 }
176
177 #[test]
178 fn no_match_denies() {
179 let caller = make_id("acme", "client", "0.1.0", 1);
180 let target = make_id("acme", "svc", "0.1.0", 1);
181 let acl = Acl {
182 rules: vec![make_rule("other", "other", "0.1.0", Permission::Allow)],
183 };
184 assert!(!check_acl_permission(Some(&caller), &target, Some(&acl)).unwrap());
185 }
186
187 #[test]
188 fn any_realm_rule_matches_foreign_realm() {
189 let caller = make_id("acme", "client", "0.1.0", 2002);
190 let target = make_id("acme", "svc", "0.1.0", 1001);
191 let acl = Acl {
192 rules: vec![AclRule {
193 permission: Permission::Allow as i32,
194 from_type: ActrType {
195 manufacturer: "acme".into(),
196 name: "client".into(),
197 version: "0.1.0".into(),
198 },
199 source_realm: Some(SourceRealm::AnyRealm(true)),
200 }],
201 };
202 assert!(check_acl_permission(Some(&caller), &target, Some(&acl)).unwrap());
203 }
204}