use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use super::audit::{AuthzAuditSink, AuthzSource, NullAuditSink};
use super::error::AuthzResult;
use super::types::{AuthzDecision, AuthzRequest};
#[async_trait]
pub trait AuthzDecisionHook: Send + Sync + std::fmt::Debug {
async fn evaluate(&self, req: AuthzRequest) -> AuthzDecision;
}
#[derive(Debug, Clone)]
pub struct DenyAllHook {
sink: Arc<dyn AuthzAuditSink>,
}
impl DenyAllHook {
pub fn new(sink: Arc<dyn AuthzAuditSink>) -> Self {
Self { sink }
}
pub fn null() -> Self {
Self {
sink: Arc::new(NullAuditSink),
}
}
}
#[async_trait]
impl AuthzDecisionHook for DenyAllHook {
async fn evaluate(&self, req: AuthzRequest) -> AuthzDecision {
let decision = AuthzDecision::Deny {
reason: "no authz hook configured".into(),
policy: AuthzSource::DenyAllDefault.policy().to_string(),
};
self.sink
.record(&req, &decision, AuthzSource::DenyAllDefault)
.await;
decision
}
}
#[derive(Debug, Clone)]
pub struct AllowAllHook {
sink: Arc<dyn AuthzAuditSink>,
}
impl AllowAllHook {
pub fn new(sink: Arc<dyn AuthzAuditSink>) -> Self {
Self { sink }
}
pub fn null() -> Self {
Self {
sink: Arc::new(NullAuditSink),
}
}
}
#[async_trait]
impl AuthzDecisionHook for AllowAllHook {
async fn evaluate(&self, req: AuthzRequest) -> AuthzDecision {
let decision = AuthzDecision::Allow;
self.sink
.record(&req, &decision, AuthzSource::AllowAllUnrestricted)
.await;
decision
}
}
#[derive(Debug, Clone)]
pub struct WebhookHook {
url: String,
timeout: Duration,
client: reqwest::Client,
sink: Arc<dyn AuthzAuditSink>,
}
impl WebhookHook {
pub fn new(url: String, timeout: Duration, sink: Arc<dyn AuthzAuditSink>) -> AuthzResult<Self> {
let client = reqwest::Client::builder().timeout(timeout).build()?;
Ok(Self {
url,
timeout,
client,
sink,
})
}
pub fn url(&self) -> &str {
&self.url
}
pub const fn timeout(&self) -> Duration {
self.timeout
}
async fn fault(&self, req: &AuthzRequest) -> AuthzDecision {
let decision = AuthzDecision::Deny {
reason: "authz hook unreachable".into(),
policy: AuthzSource::WebhookFault.policy().to_string(),
};
self.sink
.record(req, &decision, AuthzSource::WebhookFault)
.await;
decision
}
}
#[async_trait]
impl AuthzDecisionHook for WebhookHook {
async fn evaluate(&self, req: AuthzRequest) -> AuthzDecision {
let response = self.client.post(&self.url).json(&req).send().await;
let response = match response {
Ok(r) => r,
Err(err) => {
tracing::warn!(
error = %err,
url = %self.url,
"authz hook transport failure",
);
return self.fault(&req).await;
},
};
if !response.status().is_success() {
tracing::warn!(
status = response.status().as_u16(),
url = %self.url,
"authz hook returned non-success status",
);
return self.fault(&req).await;
}
match response.json::<AuthzDecision>().await {
Ok(decision) => decision,
Err(err) => {
tracing::warn!(
error = %err,
url = %self.url,
"authz hook response decode failure",
);
self.fault(&req).await
},
}
}
}