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>,
}
#[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,
}
#[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)]
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,
}
#[derive(Debug)]
pub struct CompiledSnapshot {
pub version: u64,
bindings_by_sub: HashMap<String, Vec<CompiledBinding>>,
grants_by_role: HashMap<String, Vec<CompiledGrant>>,
}
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 {
grants_by_role
.entry(g.role_id)
.or_default()
.push(CompiledGrant { permission: g.permission });
}
Self { version: payload.policy_version, bindings_by_sub, grants_by_role }
}
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 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
}