soma-som-core 0.1.0

Universal soma(som) structural primitives — Quad / Tree / Ring / Genesis / Fingerprint / TemporalLedger / CrossingRecord
Documentation
// SPDX-License-Identifier: LGPL-3.0-only
#![allow(missing_docs)]

//! Pluggable command authorization + ring-collected permission/rule tables.
//!
//! Two surfaces live here:
//!
//! 1. [`AuthorizationProvider`] — the §13.1 trait the ring delegates the
//!    authorize/deny decision to. Application code supplies a concrete
//!    provider; the ring exposes [`PermitAll`] as the no-op default.
//!
//! 2. [`PermissionRegistry`] + [`AuthorizationTable`] — string-keyed
//!    structural carriers that ring extensions populate at registration time.
//!    The application's authorization provider interprets the opaque keys
//!    against its own policy model (OPUS §13.1 — ring mechanism vs.
//!    application policy).

use std::sync::Arc;

use crate::quad::Tree;

// ── §13.1 Authorization trait ───────────────────────────────────────────────

/// Pluggable authorization decision for ring commands.
pub trait AuthorizationProvider: Send + Sync {
    fn authorize(&self, tree: &Tree, cycle: u64) -> Result<(), String>;
}

/// No-op default — permits every command. Ring applications that need
/// real authorization replace this provider at startup.
pub struct PermitAll;

impl AuthorizationProvider for PermitAll {
    fn authorize(&self, _tree: &Tree, _cycle: u64) -> Result<(), String> {
        Ok(())
    }
}

// ── Permission requirement types ────────────────────────────────────────────
//
// Companion types for ThroughRing::permission_requirements(). The
// application's authorization provider interprets the opaque string-keyed
// fields against its own policy model.

/// A permission requirement declared by a ring extension.
///
/// Fields are string-keyed so the ring tier stays free of application-specific
/// enums (OPUS §13.1 — ring mechanism). The application's authorization
/// provider interprets keys against its own policy model.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PermissionRequirement {
    pub command: String,
    pub min_role_key: String,
    pub activity_maximum_key: String,
    pub decision_tier_key: String,
    pub security_floor_key: String,
}

impl PermissionRequirement {
    pub fn new(
        command: impl Into<String>,
        min_role_key: impl Into<String>,
        activity_maximum_key: impl Into<String>,
        decision_tier_key: impl Into<String>,
        security_floor_key: impl Into<String>,
    ) -> Self {
        Self {
            command: command.into(),
            min_role_key: min_role_key.into(),
            activity_maximum_key: activity_maximum_key.into(),
            decision_tier_key: decision_tier_key.into(),
            security_floor_key: security_floor_key.into(),
        }
    }
}

/// Collision record produced by [`PermissionRegistry::register_checked`].
#[derive(Debug, Clone)]
pub struct PermissionCollision {
    pub command: String,
    pub existing_min_role_key: String,
    pub new_min_role_key: String,
}

/// Ring-collected registry of [`PermissionRequirement`]s.
#[derive(Debug, Clone, Default)]
pub struct PermissionRegistry {
    requirements: Vec<PermissionRequirement>,
}

impl PermissionRegistry {
    pub fn new() -> Self { Self::default() }
    pub fn register(&mut self, reqs: Vec<PermissionRequirement>) { self.requirements.extend(reqs); }
    pub fn lookup(&self, command_type: &str) -> Option<&PermissionRequirement> {
        self.requirements.iter().find(|r| r.command == command_type)
    }
    pub fn len(&self) -> usize { self.requirements.len() }
    pub fn is_empty(&self) -> bool { self.requirements.is_empty() }
    pub fn register_checked(&mut self, reqs: Vec<PermissionRequirement>) -> Vec<PermissionCollision> {
        let mut collisions = Vec::new();
        for req in reqs {
            if let Some(existing) = self.requirements.iter().find(|r| r.command == req.command) {
                collisions.push(PermissionCollision {
                    command: req.command.clone(),
                    existing_min_role_key: existing.min_role_key.clone(),
                    new_min_role_key: req.min_role_key.clone(),
                });
            } else {
                self.requirements.push(req);
            }
        }
        collisions
    }
    pub fn iter(&self) -> impl Iterator<Item = &PermissionRequirement> { self.requirements.iter() }
    pub fn count_by_prefix(&self, prefix: &str) -> usize {
        self.requirements.iter().filter(|r| r.command.starts_with(prefix)).count()
    }
}

pub type SharedPermissionRegistry = Arc<std::sync::Mutex<PermissionRegistry>>;

// ── Ring-level authorization table ──────────────────────────────────────────

/// A single rule in the shared authorization table (string-keyed,
/// application-neutral).
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AuthorizationTableEntry {
    pub rule_id: String,
    pub command_pattern: String,
    pub group_pattern: String,
    pub context: String,
    pub autonomy: String,
    pub tier: String,
    pub effect: String,
    pub config_layer: String,
}

/// Ring-level authorization rule table.
#[derive(Debug, Clone, Default)]
pub struct AuthorizationTable {
    rules: Vec<AuthorizationTableEntry>,
}

impl AuthorizationTable {
    pub fn new() -> Self { Self::default() }
    pub fn set_rules(&mut self, rules: Vec<AuthorizationTableEntry>) { self.rules = rules; }
    pub fn rules(&self) -> &[AuthorizationTableEntry] { &self.rules }
    pub fn len(&self) -> usize { self.rules.len() }
    pub fn is_empty(&self) -> bool { self.rules.is_empty() }
    pub fn evaluate(&self, command_type: &str, user_groups: &[&str], request_context: Option<&str>) -> Option<AuthorizationTableResult> {
        for rule in &self.rules {
            if matches_rule(rule, command_type, user_groups, request_context) {
                return Some(AuthorizationTableResult {
                    matched_rule_id: rule.rule_id.clone(),
                    autonomy: rule.autonomy.clone(),
                    tier: rule.tier.clone(),
                    effect: rule.effect.clone(),
                    config_layer: rule.config_layer.clone(),
                });
            }
        }
        None
    }
}

/// Result of [`AuthorizationTable::evaluate`].
#[derive(Debug, Clone)]
pub struct AuthorizationTableResult {
    pub matched_rule_id: String,
    pub autonomy: String,
    pub tier: String,
    pub effect: String,
    pub config_layer: String,
}

impl AuthorizationTableResult {
    pub fn is_allowed(&self) -> bool { self.effect == "allow" }
}

pub type SharedAuthorizationTable = Arc<std::sync::Mutex<AuthorizationTable>>;

fn matches_rule(rule: &AuthorizationTableEntry, command_type: &str, user_groups: &[&str], request_context: Option<&str>) -> bool {
    matches_command(&rule.command_pattern, command_type)
        && matches_group(&rule.group_pattern, user_groups)
        && matches_context(&rule.context, request_context)
}

fn matches_command(pattern: &str, command_type: &str) -> bool {
    if pattern == "*" { return true; }
    if let Some(prefix) = pattern.strip_suffix(".*") {
        return command_type.starts_with(prefix) && (command_type.len() == prefix.len() || command_type.as_bytes().get(prefix.len()) == Some(&b'.'));
    }
    if pattern.starts_with("*.") {
        let suffix = &pattern[1..];
        return command_type.ends_with(suffix);
    }
    pattern == command_type
}

fn matches_group(pattern: &str, user_groups: &[&str]) -> bool {
    if pattern == "-" { return true; }
    if let Some(prefix) = pattern.strip_suffix('*') {
        return user_groups.iter().any(|g| g.starts_with(prefix));
    }
    user_groups.contains(&pattern)
}

fn matches_context(pattern: &str, request_context: Option<&str>) -> bool {
    if pattern == "-" { return true; }
    match request_context { Some(ctx) => pattern == ctx, None => false }
}