use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Arc;
use arc_swap::ArcSwapOption;
use super::model::{grant_matches, CompiledSnapshot, Effect, SnapshotPayload};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DenyReason {
NoBinding,
EmergencyDeny,
ExplicitDeny,
NoGrant,
}
impl DenyReason {
pub fn as_str(self) -> &'static str {
match self {
DenyReason::NoBinding => "no_binding",
DenyReason::EmergencyDeny => "emergency_deny",
DenyReason::ExplicitDeny => "explicit_deny",
DenyReason::NoGrant => "no_grant",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Allow,
Deny { reason: DenyReason },
NeedsStepUp { min_acr: String },
NeedsQuorum,
}
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",
Decision::NeedsStepUp { .. } => "step_up_required",
Decision::NeedsQuorum => "quorum_required",
}
}
}
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, acr: Option<&str>, permission: &str) -> Decision {
let Some(snap) = self.snapshot.load_full() else {
return Decision::Deny { reason: DenyReason::NoBinding };
};
evaluate(&snap, sub, acr, permission, now_secs())
}
}
fn evaluate(
snap: &CompiledSnapshot,
sub: &str,
acr: Option<&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;
for g in snap.grants_for_role(role_id) {
if grant_matches(&g.permission, permission) {
match g.effect {
Effect::Deny => return Decision::Deny { reason: DenyReason::ExplicitDeny },
Effect::Allow => saw_allow = true,
}
}
}
}
if !has_active_role {
return Decision::Deny { reason: DenyReason::NoBinding };
}
if !saw_allow {
return Decision::Deny { reason: DenyReason::NoGrant };
}
if let Some(meta) = snap.perm_meta(permission) {
if let Some(min) = &meta.min_acr {
if acr != Some(min.as_str()) {
return Decision::NeedsStepUp { min_acr: min.clone() };
}
}
if meta.requires_quorum {
return Decision::NeedsQuorum;
}
}
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)
}