diff --git a/Cargo.lock b/Cargo.lock index ca3f11b..fec1da8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -270,6 +270,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "bitreader" @@ -442,10 +445,12 @@ version = "0.3.0" dependencies = [ "anyhow", "async-trait", + "bitflags 2.6.0", "chrono", "deadpool-postgres", "deadpool-sqlite", "encoding_rs", + "glob", "log", "openssl", "postgres-openssl", diff --git a/cli/src/subscriptions.rs b/cli/src/subscriptions.rs index e5d413f..4825b8d 100644 --- a/cli/src/subscriptions.rs +++ b/cli/src/subscriptions.rs @@ -617,7 +617,7 @@ async fn edit_filter(subscription: &mut SubscriptionData, matches: &ArgMatches) warn!("'{}' filter has been set without principals making this subscription apply to nothing.", op) } - subscription.set_client_filter(Some(ClientFilter::new(op, princs))); + subscription.set_client_filter(Some(ClientFilter::new_legacy(op, princs))); return Ok(()); } diff --git a/common/Cargo.toml b/common/Cargo.toml index bdf2997..91125da 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -26,6 +26,8 @@ deadpool-sqlite = "0.5.0" openssl = "0.10.66" postgres-openssl = "0.5.0" strum = { version = "0.26.1", features = ["derive"] } +bitflags = { version = "2.6.0", features = ["serde"] } +glob = "0.3.1" [dev-dependencies] tempfile = "3.14.0" @@ -68,4 +70,4 @@ assets = [ { source = "../openwec.conf.sample.toml", dest = "/usr/share/doc/openwec/", mode = "0644", doc = true }, { source = "../README.md", dest = "/usr/share/doc/openwec/", mode = "0644", doc = true }, { source = "../doc/*", dest = "/usr/share/doc/openwec/doc/", mode = "0644", doc = true }, -] \ No newline at end of file +] diff --git a/common/src/database/mod.rs b/common/src/database/mod.rs index 1f2075b..7f9666a 100644 --- a/common/src/database/mod.rs +++ b/common/src/database/mod.rs @@ -196,6 +196,8 @@ pub mod tests { .set_ignore_channel_error(false) .set_client_filter(Some(ClientFilter::from( "Only".to_string(), + "KerberosPrinc".to_string(), + None, Some("couscous,boulette".to_string()), )?)) .set_outputs(vec![ @@ -232,7 +234,7 @@ pub mod tests { ); assert_eq!( tata.client_filter().unwrap().targets(), - &HashSet::from(["couscous".to_string(), "boulette".to_string()]) + HashSet::from(["couscous", "boulette"]) ); assert_eq!( @@ -298,10 +300,10 @@ pub mod tests { ); assert_eq!( tata2.client_filter().unwrap().targets(), - &HashSet::from([ - "couscous".to_string(), - "boulette".to_string(), - "semoule".to_string() + HashSet::from([ + "couscous", + "boulette", + "semoule" ]) ); assert_eq!(tata2.is_active_for("couscous"), true); @@ -329,7 +331,7 @@ pub mod tests { ); assert_eq!( tata2_clone.client_filter().unwrap().targets(), - &HashSet::from(["boulette".to_string(), "semoule".to_string()]) + HashSet::from(["boulette", "semoule"]) ); assert_eq!(tata2_clone.is_active_for("couscous"), true); diff --git a/common/src/database/postgres.rs b/common/src/database/postgres.rs index 9cc55be..7c25f79 100644 --- a/common/src/database/postgres.rs +++ b/common/src/database/postgres.rs @@ -216,7 +216,11 @@ fn row_to_subscription(row: &Row) -> Result { let client_filter_op: Option = row.try_get("client_filter_op")?; let client_filter = match client_filter_op { - Some(op) => Some(ClientFilter::from(op, row.try_get("client_filter_value")?)?), + Some(op) => { + let client_filter_type: Option<_> = row.try_get("client_filter_type").unwrap(); + let client_filter_type = client_filter_type.unwrap_or("KerberosPrinc".to_owned()); + Some(ClientFilter::from(op, client_filter_type, row.try_get("client_filter_flags")?, row.try_get("client_filter_value")?)?) + }, None => None }; @@ -628,6 +632,8 @@ impl Database for PostgresDatabase { let max_envelope_size: i32 = subscription.max_envelope_size().try_into()?; let client_filter_op: Option = subscription.client_filter().map(|f| f.operation().to_string()); + let client_filter_type = subscription.client_filter().map(|f| f.kind().to_string()); + let client_filter_flags = subscription.client_filter().map(|f| f.flags().to_string()); let client_filter_value = subscription.client_filter().and_then(|f| f.targets_to_opt_string()); let count = self @@ -638,9 +644,9 @@ impl Database for PostgresDatabase { r#"INSERT INTO subscriptions (uuid, version, revision, name, uri, query, heartbeat_interval, connection_retry_count, connection_retry_interval, max_time, max_elements, max_envelope_size, enabled, read_existing_events, content_format, - ignore_channel_error, client_filter_op, client_filter_value, outputs, locale, + ignore_channel_error, client_filter_op, client_filter_type, client_filter_flags, client_filter_value, outputs, locale, data_locale) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23) ON CONFLICT (uuid) DO UPDATE SET version = excluded.version, revision = excluded.revision, @@ -658,6 +664,8 @@ impl Database for PostgresDatabase { content_format = excluded.content_format, ignore_channel_error = excluded.ignore_channel_error, client_filter_op = excluded.client_filter_op, + client_filter_type = excluded.client_filter_type, + client_filter_flags = excluded.client_filter_flags, client_filter_value = excluded.client_filter_value, outputs = excluded.outputs, locale = excluded.locale, @@ -680,6 +688,8 @@ impl Database for PostgresDatabase { &subscription.content_format().to_string(), &subscription.ignore_channel_error(), &client_filter_op, + &client_filter_type, + &client_filter_flags, &client_filter_value, &serde_json::to_string(subscription.outputs())?.as_str(), &subscription.locale(), diff --git a/common/src/database/schema/postgres/_014_alter_client_filter_in_subscriptions.rs b/common/src/database/schema/postgres/_014_alter_client_filter_in_subscriptions.rs index 1e97d32..53f97c5 100644 --- a/common/src/database/schema/postgres/_014_alter_client_filter_in_subscriptions.rs +++ b/common/src/database/schema/postgres/_014_alter_client_filter_in_subscriptions.rs @@ -16,12 +16,16 @@ impl PostgresMigration for AlterClientFilterInSubscriptionsTable { async fn up(&self, tx: &mut Transaction) -> Result<()> { tx.execute("ALTER TABLE subscriptions RENAME COLUMN princs_filter_op TO client_filter_op", &[]).await?; tx.execute("ALTER TABLE subscriptions RENAME COLUMN princs_filter_value TO client_filter_value", &[]).await?; + tx.execute("ALTER TABLE subscriptions ADD COLUMN client_filter_type TEXT", &[]).await?; + tx.execute("ALTER TABLE subscriptions ADD COLUMN client_filter_flags TEXT", &[]).await?; Ok(()) } async fn down(&self, tx: &mut Transaction) -> Result<()> { tx.execute("ALTER TABLE subscriptions RENAME COLUMN client_filter_op TO princs_filter_op", &[]).await?; tx.execute("ALTER TABLE subscriptions RENAME COLUMN client_filter_value TO princs_filter_value", &[]).await?; + tx.execute("ALTER TABLE subscriptions DROP COLUMN client_filter_type", &[]).await?; + tx.execute("ALTER TABLE subscriptions DROP COLUMN client_filter_flags", &[]).await?; Ok(()) } } diff --git a/common/src/database/schema/sqlite/_014_alter_client_filter_in_subscriptions.rs b/common/src/database/schema/sqlite/_014_alter_client_filter_in_subscriptions.rs index d7c7bf3..3ef7692 100644 --- a/common/src/database/schema/sqlite/_014_alter_client_filter_in_subscriptions.rs +++ b/common/src/database/schema/sqlite/_014_alter_client_filter_in_subscriptions.rs @@ -17,6 +17,10 @@ impl SQLiteMigration for AlterClientFilterInSubscriptionsTable { .map_err(|err| anyhow!("SQLiteError: {}", err))?; conn.execute("ALTER TABLE subscriptions RENAME COLUMN princs_filter_value TO client_filter_value", []) .map_err(|err| anyhow!("SQLiteError: {}", err))?; + conn.execute("ALTER TABLE subscriptions ADD COLUMN client_filter_type TEXT", []) + .map_err(|err| anyhow!("SQLiteError: {}", err))?; + conn.execute("ALTER TABLE subscriptions ADD COLUMN client_filter_flags TEXT", []) + .map_err(|err| anyhow!("SQLiteError: {}", err))?; Ok(()) } @@ -25,6 +29,10 @@ impl SQLiteMigration for AlterClientFilterInSubscriptionsTable { .map_err(|err| anyhow!("SQLiteError: {}", err))?; conn.execute("ALTER TABLE subscriptions RENAME COLUMN client_filter_value TO princs_filter_value", []) .map_err(|err| anyhow!("SQLiteError: {}", err))?; + conn.execute("ALTER TABLE subscriptions DROP COLUMN client_filter_type", []) + .map_err(|err| anyhow!("SQLiteError: {}", err))?; + conn.execute("ALTER TABLE subscriptions DROP COLUMN client_filter_flags", []) + .map_err(|err| anyhow!("SQLiteError: {}", err))?; Ok(()) } } diff --git a/common/src/database/sqlite.rs b/common/src/database/sqlite.rs index 96a46bc..8f2ed16 100644 --- a/common/src/database/sqlite.rs +++ b/common/src/database/sqlite.rs @@ -169,7 +169,11 @@ fn row_to_subscription(row: &Row) -> Result { let client_filter_op: Option = row.get("client_filter_op")?; let client_filter = match client_filter_op { - Some(op) => Some(ClientFilter::from(op, row.get("client_filter_value")?)?), + Some(op) => { + let client_filter_type: Option<_> = row.get("client_filter_type")?; + let client_filter_type = client_filter_type.unwrap_or("KerberosPrinc".to_owned()); + Some(ClientFilter::from(op, client_filter_type, row.get("client_filter_flags")?, row.get("client_filter_value")?)?) + }, None => None }; @@ -553,6 +557,8 @@ impl Database for SQLiteDatabase { async fn store_subscription(&self, subscription: &SubscriptionData) -> Result<()> { let subscription = subscription.clone(); let client_filter_op: Option = subscription.client_filter().map(|f| f.operation().to_string()); + let client_filter_type = subscription.client_filter().map(|f| f.kind().to_string()); + let client_filter_flags = subscription.client_filter().map(|f| f.flags().to_string()); let client_filter_value = subscription.client_filter().and_then(|f| f.targets_to_opt_string()); let count = self @@ -564,12 +570,12 @@ impl Database for SQLiteDatabase { r#"INSERT INTO subscriptions (uuid, version, revision, name, uri, query, heartbeat_interval, connection_retry_count, connection_retry_interval, max_time, max_elements, max_envelope_size, enabled, read_existing_events, content_format, - ignore_channel_error, client_filter_op, client_filter_value, outputs, locale, + ignore_channel_error, client_filter_op, client_filter_type, client_filter_flags, client_filter_value, outputs, locale, data_locale) VALUES (:uuid, :version, :revision, :name, :uri, :query, :heartbeat_interval, :connection_retry_count, :connection_retry_interval, :max_time, :max_elements, :max_envelope_size, :enabled, :read_existing_events, :content_format, - :ignore_channel_error, :client_filter_op, :client_filter_value, :outputs, + :ignore_channel_error, :client_filter_op, :client_filter_type, :client_filter_flags, :client_filter_value, :outputs, :locale, :data_locale) ON CONFLICT (uuid) DO UPDATE SET version = excluded.version, @@ -588,6 +594,8 @@ impl Database for SQLiteDatabase { content_format = excluded.content_format, ignore_channel_error = excluded.ignore_channel_error, client_filter_op = excluded.client_filter_op, + client_filter_type = excluded.client_filter_type, + client_filter_flags = excluded.client_filter_flags, client_filter_value = excluded.client_filter_value, outputs = excluded.outputs, locale = excluded.locale, @@ -610,6 +618,8 @@ impl Database for SQLiteDatabase { ":content_format": subscription.content_format().to_string(), ":ignore_channel_error": subscription.ignore_channel_error(), ":client_filter_op": client_filter_op, + ":client_filter_type": client_filter_type, + ":client_filter_flags": client_filter_flags, ":client_filter_value": client_filter_value, ":outputs": serde_json::to_string(subscription.outputs())?, ":locale": subscription.locale(), diff --git a/common/src/models/config.rs b/common/src/models/config.rs index 1a14a14..19b2199 100644 --- a/common/src/models/config.rs +++ b/common/src/models/config.rs @@ -178,6 +178,10 @@ impl From for crate::subscription::ClientFilterOperation #[serde(deny_unknown_fields)] struct ClientFilter { pub operation: ClientFilterOperation, + #[serde(rename = "type", default)] + pub kind: crate::subscription::ClientFilterType, + #[serde(default)] + pub flags: crate::subscription::ClientFilterFlags, #[serde(alias = "cert_subjects", alias = "princs")] pub targets: HashSet, } @@ -186,7 +190,7 @@ impl TryFrom for crate::subscription::ClientFilter { type Error = anyhow::Error; fn try_from(value: ClientFilter) -> std::prelude::v1::Result { - Ok(crate::subscription::ClientFilter::new(value.operation.into(), value.targets)) + crate::subscription::ClientFilter::try_new(value.operation.into(), value.kind, value.flags, value.targets) } } @@ -506,7 +510,9 @@ path = "/whatever/you/{ip}/want/{principal}/{ip:2}/{node}/end" let mut targets = HashSet::new(); targets.insert("toto@windomain.local".to_string()); targets.insert("tutu@windomain.local".to_string()); - let filter = crate::subscription::ClientFilter::new(crate::subscription::ClientFilterOperation::Only, targets); + let kind = crate::subscription::ClientFilterType::KerberosPrinc; + let flags = crate::subscription::ClientFilterFlags::empty(); + let filter = crate::subscription::ClientFilter::try_new(crate::subscription::ClientFilterOperation::Only, kind, flags, targets)?; expected.set_client_filter(Some(filter)); diff --git a/common/src/models/export.rs b/common/src/models/export.rs index bf112cf..31abb76 100644 --- a/common/src/models/export.rs +++ b/common/src/models/export.rs @@ -202,7 +202,7 @@ mod v1 { return None; }; - Some(crate::subscription::ClientFilter::new(op.into(), self.princs)) + Some(crate::subscription::ClientFilter::new_legacy(op.into(), self.princs)) } } @@ -549,7 +549,7 @@ pub mod v2 { return None; }; - Some(crate::subscription::ClientFilter::new(op.into(), self.princs)) + Some(crate::subscription::ClientFilter::new_legacy(op.into(), self.princs)) } } @@ -557,7 +557,7 @@ pub mod v2 { fn from(value: Option) -> Self { Self { operation: value.as_ref().and_then(|f| Some(f.operation().clone().into())), - princs: value.map_or(HashSet::new(), |f| f.targets().clone()), + princs: value.map_or(HashSet::new(), |f| f.targets().iter().cloned().map(String::from).collect()), } } } @@ -712,10 +712,12 @@ mod tests { .set_max_elements(Some(100)) .set_read_existing_events(false) .set_uri(Some("toto".to_string())) - .set_client_filter(Some(crate::subscription::ClientFilter::new( + .set_client_filter(Some(crate::subscription::ClientFilter::try_new( crate::subscription::ClientFilterOperation::Except, + crate::subscription::ClientFilterType::KerberosPrinc, + crate::subscription::ClientFilterFlags::CaseSensitive, targets, - ))) + )?)) .set_outputs(vec![crate::subscription::SubscriptionOutput::new( crate::subscription::SubscriptionOutputFormat::Json, crate::subscription::SubscriptionOutputDriver::Tcp( diff --git a/common/src/subscription.rs b/common/src/subscription.rs index 2a4da3b..cb41c28 100644 --- a/common/src/subscription.rs +++ b/common/src/subscription.rs @@ -8,8 +8,10 @@ use std::{ use anyhow::{anyhow, bail, Result, Error}; use log::{info, warn}; use serde::{Deserialize, Serialize}; -use strum::{AsRefStr, EnumString, VariantNames}; +use strum::{Display, AsRefStr, EnumString, VariantNames}; use uuid::Uuid; +use bitflags::bitflags; +use glob::Pattern; use crate::utils::VersionHasher; @@ -236,68 +238,182 @@ impl FromStr for ClientFilterOperation { } } +#[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Display, AsRefStr, EnumString)] +#[strum(ascii_case_insensitive)] +pub enum ClientFilterType { + #[default] + KerberosPrinc, + TLSCertSubject, + MachineID, +} + +bitflags! { + #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] + pub struct ClientFilterFlags: u32 { + const CaseSensitive = 1 << 0; + const GlobPattern = 1 << 1; + } +} + +impl Display for ClientFilterFlags { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + bitflags::parser::to_writer_strict(self, f) + } +} + +impl Default for ClientFilterFlags { + fn default() -> Self { + Self::empty() + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +enum ClientFilterTargets { + Exact(HashSet), + Glob(Vec) +} + #[derive(Debug, Clone, Eq, PartialEq)] pub struct ClientFilter { operation: ClientFilterOperation, - targets: HashSet, + kind: ClientFilterType, + flags: ClientFilterFlags, + targets: ClientFilterTargets, } impl ClientFilter { - pub fn new(operation: ClientFilterOperation, targets: HashSet) -> Self { - Self { operation, targets } + pub fn new_legacy(operation: ClientFilterOperation, targets: HashSet) -> Self { + Self { + operation, + kind: ClientFilterType::KerberosPrinc, + flags: ClientFilterFlags::CaseSensitive, + targets: ClientFilterTargets::Exact(targets), + } + } + + pub fn try_new(operation: ClientFilterOperation, kind: ClientFilterType, flags: ClientFilterFlags, targets: HashSet) -> Result { + let targets = if flags.contains(ClientFilterFlags::GlobPattern) { + ClientFilterTargets::Glob(targets.iter().map(|t| Pattern::new(t)).collect::, _>>()?) + } else { + ClientFilterTargets::Exact(targets) + }; + + Ok(Self { operation, kind, flags, targets }) } - pub fn from(operation: String, targets: Option) -> Result { + pub fn from(operation: String, kind: String, flags: Option, targets: Option) -> Result { + let flags: ClientFilterFlags = bitflags::parser::from_str_strict(flags.unwrap_or_default().as_str()).map_err(|e| anyhow!("{:?}", e))?; + + let mut t = if flags.contains(ClientFilterFlags::GlobPattern) { + ClientFilterTargets::Glob(Vec::new()) + } else { + ClientFilterTargets::Exact(HashSet::new()) + }; + + if let Some(targets) = targets { + let targets = targets.split(','); + + t = if flags.contains(ClientFilterFlags::GlobPattern) { + ClientFilterTargets::Glob(targets.map(|t| Pattern::new(t)).collect::, _>>()?) + } else { + ClientFilterTargets::Exact(HashSet::from_iter(targets.map(|s| s.to_string()))) + }; + } + Ok(ClientFilter { - operation: operation.parse()?, - targets: match targets { - Some(p) => HashSet::from_iter(p.split(',').map(|s| s.to_string())), - None => HashSet::new(), - }, + operation: operation.parse()?, kind: kind.parse()?, flags, targets: t }) } + fn matches(&self, target: &str) -> bool { + match &self.targets { + ClientFilterTargets::Exact(targets) => targets.contains(target), + ClientFilterTargets::Glob(targets) => { + for p in targets { + if p.matches(target) { + return true; + } + } + + false + } + } + } + pub fn eval(&self, target: &str) -> bool { match self.operation { - ClientFilterOperation::Only => self.targets.contains(target), - ClientFilterOperation::Except => !self.targets.contains(target), + ClientFilterOperation::Only => self.matches(target), + ClientFilterOperation::Except => !self.matches(target), } } - pub fn targets(&self) -> &HashSet { - &self.targets + pub fn targets(&self) -> HashSet<&str> { + match &self.targets { + ClientFilterTargets::Exact(targets) => targets.iter().map(|t| t.as_str()).collect(), + ClientFilterTargets::Glob(targets) => targets.iter().map(|t| t.as_str()).collect(), + } } pub fn targets_to_string(&self) -> String { self.targets() .iter() .cloned() + .map(String::from) .collect::>() .join(",") } pub fn targets_to_opt_string(&self) -> Option { - if self.targets().is_empty() { - None - } else { - Some(self.targets_to_string()) + match &self.targets { + ClientFilterTargets::Exact(targets) => { + if targets.is_empty() { + return None; + } + }, + ClientFilterTargets::Glob(targets) => { + if targets.is_empty() { + return None; + } + } } + + Some(self.targets_to_string()) } pub fn add_target(&mut self, target: &str) -> Result<()> { - self.targets.insert(target.to_owned()); + match &mut self.targets { + ClientFilterTargets::Exact(targets) => { targets.insert(target.to_owned()); }, + ClientFilterTargets::Glob(targets) => { targets.push(Pattern::new(target)?); }, + } Ok(()) } pub fn delete_target(&mut self, target: &str) -> Result<()> { - if !self.targets.remove(target) { - warn!("{} was not present in the targets set", target) + match &mut self.targets { + ClientFilterTargets::Exact(targets) => { + if !targets.remove(target) { + warn!("{} was not present in the targets set", target) + } + }, + ClientFilterTargets::Glob(targets) => { + let Some(i) = targets.iter().position(|p| p.as_str() == target) else { + warn!("{} was not present in the principals set", target); + return Ok(()); + }; + + targets.remove(i); + }, } + Ok(()) } pub fn set_targets(&mut self, targets: HashSet) -> Result<()> { - self.targets = targets; + match &mut self.targets { + ClientFilterTargets::Exact(t) => *t = targets, + ClientFilterTargets::Glob(t) => *t = targets.iter().map(|t| Pattern::new(t)).collect::, _>>()?, + } + Ok(()) } @@ -308,6 +424,14 @@ impl ClientFilter { pub fn set_operation(&mut self, operation: ClientFilterOperation) { self.operation = operation; } + + pub fn kind(&self) -> &ClientFilterType { + &self.kind + } + + pub fn flags(&self) -> &ClientFilterFlags { + &self.flags + } } #[derive(Debug, Clone, Eq, PartialEq, Hash)]