#[derive(Debug, Clone, Default, PartialEq)]
pub struct PrefixPolicy {
local: Vec<String>,
local_except: Vec<String>,
local_only: Vec<String>,
zone: Vec<String>,
}
fn split(spec: &str) -> Vec<String> {
spec.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
}
impl PrefixPolicy {
pub fn new(local: &str, local_except: &str, local_only: &str, zone: &str) -> Self {
PrefixPolicy {
local: split(local),
local_except: split(local_except),
local_only: split(local_only),
zone: split(zone),
}
}
pub fn is_zone_scoped(&self, key: &[u8]) -> bool {
if self.zone.is_empty() {
return false;
}
let k = String::from_utf8_lossy(key);
self.zone.iter().any(|p| k.starts_with(p.as_str()))
}
pub fn propagates_to_zone(&self, key: &[u8], my_zone: &str, peer_zone: &str) -> bool {
if !self.propagates(key) {
return false;
}
if !self.is_zone_scoped(key) {
return true;
}
my_zone.trim() == peer_zone.trim()
}
pub fn is_empty(&self) -> bool {
self.local.is_empty() && self.local_only.is_empty() && self.zone.is_empty()
}
pub fn propagates(&self, key: &[u8]) -> bool {
if self.local.is_empty() {
return true;
}
let k = String::from_utf8_lossy(key);
if !self.local.iter().any(|p| k.starts_with(p.as_str())) {
return true;
}
self.local_except.iter().any(|p| k.starts_with(p.as_str()))
}
pub fn may_serve(&self, key: &[u8], remote: Option<std::net::SocketAddr>) -> bool {
if self.local_only.is_empty() {
return true;
}
let k = String::from_utf8_lossy(key);
if !self.local_only.iter().any(|p| k.starts_with(p.as_str())) {
return true;
}
match remote {
None => true,
Some(a) => a.ip().is_loopback(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn skv() -> PrefixPolicy {
PrefixPolicy::new(
"/secret/",
"/secret/encrypted/,/secret/inquiry/,/secret/@meta/",
"/secret/",
"/secret/",
)
}
#[test]
fn an_unconfigured_policy_changes_nothing() {
let p = PrefixPolicy::default();
assert!(p.is_empty());
assert!(p.propagates(b"/secret/db_pw"));
assert!(p.may_serve(b"/secret/db_pw", "10.0.0.7:1".parse().ok()));
}
#[test]
fn the_subtree_is_local_and_the_exceptions_travel() {
let p = skv();
assert!(!p.propagates(b"/secret/db_pw"), "the bare namespace is local");
assert!(!p.propagates(b"/secret/share/db_pw"), "share is a local drop point");
assert!(p.propagates(b"/secret/encrypted/db_pw"));
assert!(p.propagates(b"/secret/inquiry/db_pw"));
assert!(p.propagates(b"/secret/@meta/keyver"));
}
#[test]
fn everything_outside_the_subtree_is_untouched() {
let p = skv();
assert!(p.propagates(b"/q/ch:orders/1"));
assert!(p.propagates(b"/cluster/peers/abc"));
assert!(p.may_serve(b"/q/ch:orders/1", "10.0.0.7:1".parse().ok()));
assert!(p.may_serve(b"/cluster/peers/abc", "10.0.0.7:1".parse().ok()));
}
#[test]
fn a_new_branch_fails_safe() {
let p = skv();
assert!(!p.propagates(b"/secret/newthing/db_pw"));
assert!(!p.propagates(b"/secret/encrypt/db_pw"));
}
#[test]
fn the_subtree_is_refused_over_the_network() {
let p = skv();
let far: std::net::SocketAddr = "10.0.0.7:2379".parse().unwrap();
for k in [&b"/secret/db_pw"[..], b"/secret/encrypted/db_pw", b"/secret/@meta/keyver"] {
assert!(!p.may_serve(k, Some(far)), "{} served remotely", String::from_utf8_lossy(k));
}
for lo in ["127.0.0.1:2379", "[::1]:2379"] {
assert!(p.may_serve(b"/secret/db_pw", lo.parse().ok()));
}
assert!(p.may_serve(b"/secret/db_pw", None));
}
#[test]
fn the_two_policies_are_independent() {
let p = skv();
assert!(p.propagates(b"/secret/encrypted/db_pw"));
assert!(!p.may_serve(b"/secret/encrypted/db_pw", "10.0.0.7:1".parse().ok()));
}
#[test]
fn a_zone_scoped_key_stays_in_its_zone() {
let p = skv();
assert!(p.propagates_to_zone(b"/secret/encrypted/db_pw", "east", "east"));
assert!(!p.propagates_to_zone(b"/secret/encrypted/db_pw", "east", "west"));
assert!(!p.propagates_to_zone(b"/secret/@meta/keyver", "east", "west"),
"@meta is zone-scoped too, which is what gives each zone its own key version");
assert!(p.propagates_to_zone(b"/secret/encrypted/db_pw", "", ""));
assert!(!p.propagates_to_zone(b"/secret/db_pw", "east", "east"));
assert!(p.propagates_to_zone(b"/q/ch:orders/1", "east", "west"));
assert!(!p.is_zone_scoped(b"/q/ch:orders/1"));
}
#[test]
fn an_unzoned_policy_does_not_scope_anything() {
let p = PrefixPolicy::new("/secret/", "/secret/encrypted/", "/secret/", "");
assert!(!p.is_zone_scoped(b"/secret/encrypted/db_pw"));
assert!(p.propagates_to_zone(b"/secret/encrypted/db_pw", "east", "west"));
}
#[test]
fn a_non_utf8_key_is_handled_and_not_panicked_on() {
let p = skv();
assert!(p.propagates(&[0xff, 0xfe, 0x00]));
assert!(p.may_serve(&[0xff, 0xfe, 0x00], "10.0.0.7:1".parse().ok()));
}
}