use std::collections::HashMap;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct SnapshotEnvelope {
pub payload: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SnapshotPayload {
pub schema: String,
pub policy_version: u64,
#[serde(default)]
pub issued_at: Option<String>,
#[serde(default)]
pub permissions: Vec<PermissionEntry>,
#[serde(default)]
pub roles: Vec<RoleEntry>,
#[serde(default)]
pub grants: Vec<GrantEntry>,
#[serde(default)]
pub bindings: Vec<BindingEntry>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PermissionEntry {
pub permission: String,
#[serde(default)]
pub declaring_service: Option<String>,
#[serde(default)]
pub min_acr: Option<String>,
#[serde(default)]
pub requires_quorum: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RoleEntry {
pub id: String,
pub name: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct GrantEntry {
pub role_id: String,
pub permission: String,
pub effect: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct BindingEntry {
#[serde(default)]
pub id: Option<String>,
pub principal_sub: String,
pub binding_type: String,
#[serde(default)]
pub role_id: Option<String>,
#[serde(default)]
pub expires_at: Option<i64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Effect {
Allow,
Deny,
}
#[derive(Debug, Clone)]
pub struct CompiledBinding {
pub is_emergency_deny: bool,
pub role_id: Option<String>,
pub expires_at: Option<i64>,
}
impl CompiledBinding {
pub fn active(&self, now: i64) -> bool {
self.expires_at.map(|e| e > now).unwrap_or(true)
}
}
#[derive(Debug, Clone)]
pub struct CompiledGrant {
pub permission: String,
pub effect: Effect,
}
#[derive(Debug, Clone, Default)]
pub struct PermMeta {
pub min_acr: Option<String>,
pub requires_quorum: bool,
}
#[derive(Debug)]
pub struct CompiledSnapshot {
pub version: u64,
bindings_by_sub: HashMap<String, Vec<CompiledBinding>>,
grants_by_role: HashMap<String, Vec<CompiledGrant>>,
perm_meta: HashMap<String, PermMeta>,
}
impl CompiledSnapshot {
pub fn from_payload(payload: SnapshotPayload) -> Self {
let mut bindings_by_sub: HashMap<String, Vec<CompiledBinding>> = HashMap::new();
for b in payload.bindings {
bindings_by_sub
.entry(b.principal_sub)
.or_default()
.push(CompiledBinding {
is_emergency_deny: b.binding_type.eq_ignore_ascii_case("EMERGENCY_DENY"),
role_id: b.role_id,
expires_at: b.expires_at,
});
}
let mut grants_by_role: HashMap<String, Vec<CompiledGrant>> = HashMap::new();
for g in payload.grants {
let effect = if g.effect.eq_ignore_ascii_case("DENY") {
Effect::Deny
} else {
Effect::Allow
};
grants_by_role
.entry(g.role_id)
.or_default()
.push(CompiledGrant { permission: g.permission, effect });
}
let perm_meta = payload
.permissions
.into_iter()
.map(|p| {
(
p.permission,
PermMeta { min_acr: p.min_acr, requires_quorum: p.requires_quorum },
)
})
.collect();
Self { version: payload.policy_version, bindings_by_sub, grants_by_role, perm_meta }
}
pub fn bindings_for(&self, sub: &str) -> &[CompiledBinding] {
self.bindings_by_sub.get(sub).map(|v| v.as_slice()).unwrap_or(&[])
}
pub fn grants_for_role(&self, role_id: &str) -> &[CompiledGrant] {
self.grants_by_role.get(role_id).map(|v| v.as_slice()).unwrap_or(&[])
}
pub fn perm_meta(&self, permission: &str) -> Option<&PermMeta> {
self.perm_meta.get(permission)
}
}
pub fn grant_matches(pattern: &str, permission: &str) -> bool {
if pattern == permission || pattern == "*:*" {
return true;
}
if let Some(domain) = pattern.strip_suffix(":*") {
return permission
.split_once(':')
.map(|(req_domain, _)| req_domain == domain)
.unwrap_or(false);
}
false
}