use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use rdkafka::config::ClientConfig;
use rdkafka::consumer::{Consumer, StreamConsumer};
use rdkafka::{Message, Offset, TopicPartitionList};
use super::pdp::Pdp;
use super::verify::SnapshotVerifier;
pub struct SnapshotConsumerConfig {
pub brokers: String,
pub topic: String,
}
fn apply_envelope(pdp: &Pdp, verifier: &SnapshotVerifier, bytes: &[u8]) -> bool {
let current = pdp.version().unwrap_or(0);
match verifier.verify(bytes, current) {
Ok(payload) => {
let version = payload.policy_version;
pdp.install(payload);
#[cfg(feature = "observability")]
tracing::info!(version, "authz snapshot installed");
let _ = version;
true
}
Err(super::verify::VerifyError::Stale { .. }) => false,
Err(e) => {
#[cfg(feature = "observability")]
tracing::warn!(error = %e, "authz snapshot rejected");
let _ = e;
false
}
}
}
pub fn spawn_snapshot_consumer<F, Fut>(
pdp: Arc<Pdp>,
verifier: SnapshotVerifier,
cfg: SnapshotConsumerConfig,
bootstrap: F,
) -> tokio::task::JoinHandle<()>
where
F: Fn() -> Fut + Send + 'static,
Fut: Future<Output = anyhow::Result<Option<Vec<u8>>>> + Send,
{
tokio::spawn(async move {
for attempt in 0..30u32 {
match bootstrap().await {
Ok(Some(bytes)) => {
if apply_envelope(&pdp, &verifier, &bytes) {
break;
}
break; }
Ok(None) => {}
Err(e) => {
#[cfg(feature = "observability")]
tracing::warn!(error = %e, attempt, "authz bootstrap fetch failed");
let _ = (e, attempt);
}
}
tokio::time::sleep(Duration::from_secs(2)).await;
}
loop {
match build_consumer(&cfg) {
Ok(consumer) => {
#[cfg(feature = "observability")]
tracing::info!(topic = %cfg.topic, "authz snapshot consumer connected");
loop {
match consumer.recv().await {
Ok(msg) => {
if let Some(bytes) = msg.payload() {
apply_envelope(&pdp, &verifier, bytes);
}
}
Err(e) => {
#[cfg(feature = "observability")]
tracing::warn!(error = %e, "authz consumer recv error; reconnecting");
let _ = e;
break;
}
}
}
}
Err(e) => {
#[cfg(feature = "observability")]
tracing::warn!(error = %e, "authz consumer connect failed; retrying");
let _ = e;
}
}
tokio::time::sleep(Duration::from_secs(3)).await;
}
})
}
fn build_consumer(cfg: &SnapshotConsumerConfig) -> Result<StreamConsumer, anyhow::Error> {
let group = format!("authz-snapshot-{}", uuid::Uuid::new_v4());
let consumer: StreamConsumer = ClientConfig::new()
.set("bootstrap.servers", &cfg.brokers)
.set("group.id", group)
.set("enable.auto.commit", "false")
.set("enable.partition.eof", "false")
.create()?;
let mut tpl = TopicPartitionList::new();
tpl.add_partition_offset(&cfg.topic, 0, Offset::Beginning)?;
consumer.assign(&tpl)?;
Ok(consumer)
}