road-runner-common 0.11.0

Shared Rust utilities for exchange ecosystem backend services.
Documentation
//! Wire types for the authorization snapshot published by cex-policy, plus a
//! compiled, index-optimized form the PDP evaluates in O(1).
//!
//! These deliberately re-declare the shape rather than share a crate with the
//! policy service: the contract is the JSON, not a Rust type. Keep field names in
//! sync with `cex-policy`'s snapshot builder.

use std::collections::HashMap;

use serde::Deserialize;

/// Snapshot envelope: carries the serialized `payload` string. Consumed
/// channel-trusted (no signature); any extra fields on the wire are ignored.
#[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,
    /// `ALLOW` or `DENY`.
    pub effect: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct BindingEntry {
    #[serde(default)]
    pub id: Option<String>,
    pub principal_sub: String,
    /// `ROLE` or `EMERGENCY_DENY`.
    pub binding_type: String,
    #[serde(default)]
    pub role_id: Option<String>,
    /// Unix epoch seconds; `None` = permanent.
    #[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,
}

/// Index-optimized, immutable snapshot the PDP swaps in atomically.
#[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)
    }
}

/// True when a grant pattern matches a requested permission. Supports exact,
/// `domain:*` (action wildcard), and `*:*` (super). No other wildcards.
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
}