use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Arc;
use arc_swap::ArcSwapOption;
use super::model::{grant_matches, CompiledSnapshot, SnapshotPayload};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DenyReason {
NoBinding,
EmergencyDeny,
NoGrant,
}
impl DenyReason {
pub fn as_str(self) -> &'static str {
match self {
DenyReason::NoBinding => "no_binding",
DenyReason::EmergencyDeny => "emergency_deny",
DenyReason::NoGrant => "no_grant",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Allow,
Deny { reason: DenyReason },
}
impl Decision {
pub fn is_allow(&self) -> bool {
matches!(self, Decision::Allow)
}
pub fn label(&self) -> &'static str {
match self {
Decision::Allow => "allow",
Decision::Deny { .. } => "deny",
}
}
}
pub struct Pdp {
snapshot: ArcSwapOption<CompiledSnapshot>,
last_updated_ms: AtomicI64,
}
impl Default for Pdp {
fn default() -> Self {
Self::new()
}
}
impl Pdp {
pub fn new() -> Self {
Self {
snapshot: ArcSwapOption::empty(),
last_updated_ms: AtomicI64::new(0),
}
}
pub fn install(&self, payload: SnapshotPayload) {
let compiled = Arc::new(CompiledSnapshot::from_payload(payload));
self.snapshot.store(Some(compiled));
self.last_updated_ms.store(now_millis(), Ordering::Relaxed);
}
pub fn ready(&self) -> bool {
self.snapshot.load().is_some()
}
pub fn version(&self) -> Option<u64> {
self.snapshot.load().as_ref().map(|s| s.version)
}
pub fn last_updated_ms(&self) -> i64 {
self.last_updated_ms.load(Ordering::Relaxed)
}
pub fn has_any_binding(&self, sub: &str) -> bool {
let Some(snap) = self.snapshot.load_full() else {
return false;
};
let now = now_secs();
let mut has_role = false;
for b in snap.bindings_for(sub) {
if !b.active(now) {
continue;
}
if b.is_emergency_deny {
return false;
}
if b.role_id.is_some() {
has_role = true;
}
}
has_role
}
pub fn check(&self, sub: &str, permission: &str) -> Decision {
let Some(snap) = self.snapshot.load_full() else {
return Decision::Deny { reason: DenyReason::NoBinding };
};
evaluate(&snap, sub, permission, now_secs())
}
}
fn evaluate(snap: &CompiledSnapshot, sub: &str, permission: &str, now: i64) -> Decision {
let mut has_active_role = false;
let mut saw_allow = false;
for b in snap.bindings_for(sub) {
if !b.active(now) {
continue;
}
if b.is_emergency_deny {
return Decision::Deny { reason: DenyReason::EmergencyDeny };
}
let Some(role_id) = b.role_id.as_deref() else {
continue;
};
has_active_role = true;
if snap
.grants_for_role(role_id)
.iter()
.any(|g| grant_matches(&g.permission, permission))
{
saw_allow = true;
}
}
if !has_active_role {
return Decision::Deny { reason: DenyReason::NoBinding };
}
if !saw_allow {
return Decision::Deny { reason: DenyReason::NoGrant };
}
Decision::Allow
}
fn now_secs() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
fn now_millis() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}