diff --git a/Cargo.lock b/Cargo.lock index 86edf77..12d0837 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -446,6 +446,7 @@ dependencies = [ "deadpool-postgres", "deadpool-sqlite", "encoding_rs", + "glob", "log", "openssl", "postgres-openssl", diff --git a/cli/src/skell.rs b/cli/src/skell.rs index 3d9b5af..6a9c34a 100644 --- a/cli/src/skell.rs +++ b/cli/src/skell.rs @@ -128,11 +128,12 @@ fn get_filter() -> String { # - "Except": everyone but the listed principals will be able to read the subscription # # By default, everyone can read the subscription. +# Wildcard (*, ?) patterns are allowed. # -# Example to only authorize "courgette@REALM" and "radis@REALM" to read the subscription. +# Example to only authorize "courgette@REALM" and "radis*@REALM" to read the subscription. # [filter] # operation = "Only" -# princs = ["courgette@REALM", "radis@REALM"] +# princs = ["courgette@REALM", "radis*@REALM"] "# .to_string() diff --git a/common/Cargo.toml b/common/Cargo.toml index baf4ae0..7aef47b 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -26,6 +26,7 @@ deadpool-sqlite = "0.5.0" openssl = "0.10.66" postgres-openssl = "0.5.0" strum = { version = "0.26.1", features = ["derive"] } +glob = "0.3.1" [dev-dependencies] tempfile = "3.14.0" diff --git a/common/src/models/export.rs b/common/src/models/export.rs index f6605c7..5389f48 100644 --- a/common/src/models/export.rs +++ b/common/src/models/export.rs @@ -198,7 +198,7 @@ mod v1 { impl From for crate::subscription::PrincsFilter { fn from(value: PrincsFilter) -> Self { - crate::subscription::PrincsFilter::new(value.operation.map(|x| x.into()), value.princs) + crate::subscription::PrincsFilter::new(value.operation.map(|x| x.into()), value.princs, vec![]) } } @@ -283,6 +283,7 @@ pub mod v2 { use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use uuid::Uuid; + use glob::Pattern; #[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)] pub(super) struct KafkaConfiguration { @@ -537,11 +538,13 @@ pub mod v2 { pub(super) struct PrincsFilter { pub operation: Option, pub princs: HashSet, + pub princ_globs: Vec } impl From for crate::subscription::PrincsFilter { fn from(value: PrincsFilter) -> Self { - crate::subscription::PrincsFilter::new(value.operation.map(|x| x.into()), value.princs) + let princ_globs = value.princ_globs.iter().map(|p| Pattern::new(p).unwrap()).collect(); + crate::subscription::PrincsFilter::new(value.operation.map(|x| x.into()), value.princs, princ_globs) } } @@ -550,6 +553,7 @@ pub mod v2 { Self { operation: value.operation().map(|x| x.clone().into()), princs: value.princ_literals().clone(), + princ_globs: value.princ_globs().iter().map(|p| p.as_str().to_owned()).collect() } } } @@ -707,6 +711,7 @@ mod tests { .set_princs_filter(crate::subscription::PrincsFilter::new( Some(crate::subscription::PrincsFilterOperation::Except), princs, + vec![], )) .set_outputs(vec![crate::subscription::SubscriptionOutput::new( crate::subscription::SubscriptionOutputFormat::Json, diff --git a/common/src/subscription.rs b/common/src/subscription.rs index b00022e..b31252a 100644 --- a/common/src/subscription.rs +++ b/common/src/subscription.rs @@ -10,6 +10,7 @@ use log::{info, warn}; use serde::{Deserialize, Serialize}; use strum::{AsRefStr, EnumString, VariantNames}; use uuid::Uuid; +use glob::Pattern; use crate::utils::VersionHasher; @@ -238,6 +239,11 @@ impl PrincsFilterOperation { pub struct PrincsFilter { operation: Option, princs: HashSet, + princ_globs: Vec +} + +fn has_wildcard(s: &str) -> bool { + s.chars().any(|c| c == '*' || c == '?') } impl PrincsFilter { @@ -245,31 +251,59 @@ impl PrincsFilter { PrincsFilter { operation: None, princs: HashSet::new(), + princ_globs: Vec::new(), } } - pub fn new(operation: Option, princs: HashSet) -> Self { - Self { operation, princs } + pub fn new(operation: Option, princs: HashSet, princ_globs: Vec) -> Self { + Self { operation, princs, princ_globs } } - pub fn from(operation: Option, princs: Option) -> Result { + pub fn from(operation: Option, princ_patterns: Option) -> Result { + let operation = match operation { + Some(op) => PrincsFilterOperation::opt_from_str(&op)?, + None => None, + }; + + let Some(princ_patterns) = princ_patterns else { + return Ok(PrincsFilter { + operation, + princs: HashSet::new(), + princ_globs: Vec::new(), + }); + }; + + let (princ_globs, princs): (Vec<_>, Vec<_>) = princ_patterns.split(',').partition(|&p| has_wildcard(p)); + + let princs = princs.iter().map(|p| p.to_string()); + let princ_globs = princ_globs.iter().map(|p| Pattern::new(p)).collect::, _>>()?; + Ok(PrincsFilter { - operation: match operation { - Some(op) => PrincsFilterOperation::opt_from_str(&op)?, - None => None, - }, - princs: match princs { - Some(p) => HashSet::from_iter(p.split(',').map(|s| s.to_string())), - None => HashSet::new(), - }, + operation, + princs: HashSet::from_iter(princs), + princ_globs, }) } + fn matches(&self, principal: &str) -> bool { + if self.princs.contains(principal) { + return true; + } + + for p in &self.princ_globs { + if p.matches(principal) { + return true; + } + } + + false + } + pub fn eval(&self, principal: &str) -> bool { match self.operation { None => true, - Some(PrincsFilterOperation::Only) => self.princs.contains(principal), - Some(PrincsFilterOperation::Except) => !self.princs.contains(principal), + Some(PrincsFilterOperation::Only) => self.matches(principal), + Some(PrincsFilterOperation::Except) => !self.matches(principal), } } @@ -277,16 +311,19 @@ impl PrincsFilter { &self.princs } + pub fn princ_globs(&self) -> &Vec { + &self.princ_globs + } + pub fn princs_to_string(&self) -> String { - self.princs - .iter() - .cloned() + self.princs.iter().cloned() + .chain(self.princ_globs.iter().map(|p| p.as_str().to_owned())) .collect::>() .join(",") } pub fn princs_to_opt_string(&self) -> Option { - if self.princs.is_empty() { + if self.princs.is_empty() && self.princ_globs.is_empty() { None } else { Some(self.princs_to_string()) @@ -297,6 +334,12 @@ impl PrincsFilter { if self.operation.is_none() { bail!("Could not add a principal to an unset filter") } + + if has_wildcard(princ) { + self.princ_globs.push(Pattern::new(princ)?); + return Ok(()) + } + self.princs.insert(princ.to_owned()); Ok(()) } @@ -305,17 +348,33 @@ impl PrincsFilter { if self.operation.is_none() { bail!("Could not delete a principal of an unset filter") } + + if has_wildcard(princ) { + let Some(i) = self.princ_globs.iter().position(|p| p.as_str() == princ) else { + warn!("{} was not present in the principals set", princ); + return Ok(()) + }; + + self.princ_globs.remove(i); + return Ok(()) + } + if !self.princs.remove(princ) { warn!("{} was not present in the principals set", princ) } Ok(()) } - pub fn set_princs(&mut self, princs: HashSet) -> Result<()> { + pub fn set_princs(&mut self, princ_patterns: HashSet) -> Result<()> { if self.operation.is_none() { bail!("Could not set principals of an unset filter") } + + let (princ_globs, princs): (HashSet<_>, HashSet<_>) = princ_patterns.iter().cloned().partition(|p| has_wildcard(p)); + let princ_globs = princ_globs.iter().map(|p| Pattern::new(p)).collect::, _>>()?; + self.princs = princs; + self.princ_globs = princ_globs; Ok(()) } diff --git a/subscription.sample.toml b/subscription.sample.toml index 6e82630..89516cd 100644 --- a/subscription.sample.toml +++ b/subscription.sample.toml @@ -85,11 +85,12 @@ query = """ # - "Except": everyone but the listed principals will be able to read the subscription # # By default, everyone can read the subscription. +# Wildcard (*, ?) patterns are allowed. # -# Example to only authorize "courgette@REALM" and "radis@REALM" to read the subscription. +# Example to only authorize "courgette@REALM" and "radis*@REALM" to read the subscription. # [filter] # operation = "Only" -# princs = ["courgette@REALM", "radis@REALM"] +# princs = ["courgette@REALM", "radis*@REALM"] #