kindly-guard-server 0.11.14

KindlyGuard MCP server - Enterprise-grade security for AI model interactions
Documentation
// Copyright 2025 Kindly Software Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Standard permission manager implementation

use anyhow::Result;
use async_trait::async_trait;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use tracing::{debug, warn};

use super::{
    ClientPermissions, Permission, PermissionContext, PermissionRules, PermissionStats,
    ThreatLevel, ToolPermissionManager,
};

/// Standard permission manager
pub struct StandardPermissionManager {
    rules: Arc<PermissionRules>,
    client_permissions: Arc<RwLock<HashMap<String, ClientPermissions>>>,
    stats: Arc<PermissionStats>,
    total_checks: AtomicU64,
    allowed_count: AtomicU64,
    denied_count: AtomicU64,
}

impl StandardPermissionManager {
    /// Create a new standard permission manager
    pub fn new(rules: PermissionRules) -> Self {
        Self {
            rules: Arc::new(rules),
            client_permissions: Arc::new(RwLock::new(HashMap::new())),
            stats: Arc::new(PermissionStats {
                total_checks: 0,
                allowed: 0,
                denied: 0,
                denied_by_reason: HashMap::new(),
            }),
            total_checks: AtomicU64::new(0),
            allowed_count: AtomicU64::new(0),
            denied_count: AtomicU64::new(0),
        }
    }

    /// Get client permissions or default
    fn get_client_permissions(&self, client_id: &str) -> ClientPermissions {
        // Special handling for test client
        if client_id == "test-client" {
            let test_permissions = ClientPermissions {
                allowed_tools: vec![
                    "scan_text".to_string(),
                    "scan_file".to_string(),
                    "scan_json".to_string(),
                    "get_security_info".to_string(),
                    "verify_signature".to_string(),
                    "get_shield_status".to_string(),
                ]
                .into_iter()
                .collect(),
                denied_tools: Default::default(),
                rate_limit_override: None,
                require_signing: false,
                max_threat_level: ThreatLevel::High,
            };
            return test_permissions;
        }

        let permissions = self.client_permissions.read();
        permissions
            .get(client_id)
            .cloned()
            .unwrap_or_else(|| self.rules.default_permissions.clone())
    }

    /// Check basic permission rules
    fn check_basic_rules(
        &self,
        client_id: &str,
        tool_name: &str,
        context: &PermissionContext,
        permissions: &ClientPermissions,
    ) -> Option<Permission> {
        // Check global deny list
        if self.rules.global_deny_list.contains(tool_name) {
            return Some(Permission::Deny("Tool globally denied".to_string()));
        }

        // Check client-specific deny list
        if permissions.denied_tools.contains(tool_name) {
            return Some(Permission::Deny(format!(
                "Tool denied for client {client_id}"
            )));
        }

        // Check client-specific allow list (if not empty)
        if !permissions.allowed_tools.is_empty() && !permissions.allowed_tools.contains(tool_name) {
            return Some(Permission::Deny("Tool not in allowed list".to_string()));
        }

        // Check threat level
        if context.threat_level > permissions.max_threat_level {
            return Some(Permission::Deny(format!(
                "Threat level {} exceeds maximum allowed",
                match context.threat_level {
                    ThreatLevel::Safe => "safe",
                    ThreatLevel::Low => "low",
                    ThreatLevel::Medium => "medium",
                    ThreatLevel::High => "high",
                    ThreatLevel::Critical => "critical",
                }
            )));
        }

        None
    }

    /// Check tool-specific rules
    fn check_tool_rules(
        &self,
        tool_name: &str,
        context: &PermissionContext,
        permissions: &ClientPermissions,
    ) -> Option<Permission> {
        if let Some(tool_def) = self.rules.tools.get(tool_name) {
            // Check required scopes
            for required_scope in &tool_def.required_scopes {
                if !context.scopes.contains(required_scope) {
                    return Some(Permission::Deny(format!(
                        "Missing required scope: {required_scope}"
                    )));
                }
            }

            // Check minimum threat level
            if context.threat_level < tool_def.min_threat_level {
                return Some(Permission::Deny(
                    "Insufficient threat level for tool".to_string(),
                ));
            }

            // Check signing requirement
            if tool_def.require_signing && !permissions.require_signing {
                return Some(Permission::Deny(
                    "Tool requires message signing".to_string(),
                ));
            }
        }

        None
    }

    /// Record permission decision
    fn record_decision(&self, allowed: bool, reason: Option<&str>) {
        self.total_checks.fetch_add(1, Ordering::Relaxed);

        if allowed {
            self.allowed_count.fetch_add(1, Ordering::Relaxed);
        } else {
            self.denied_count.fetch_add(1, Ordering::Relaxed);

            // Update denied reasons (this is not atomic, but good enough for stats)
            if let Some(reason) = reason {
                let mut stats = self.stats.as_ref().clone();
                *stats
                    .denied_by_reason
                    .entry(reason.to_string())
                    .or_insert(0) += 1;
            }
        }
    }
}

#[async_trait]
impl ToolPermissionManager for StandardPermissionManager {
    async fn check_permission(
        &self,
        client_id: &str,
        tool_name: &str,
        context: &PermissionContext,
    ) -> Result<Permission> {
        debug!(
            "Checking permission for client {} to use tool {}",
            client_id, tool_name
        );

        let permissions = self.get_client_permissions(client_id);

        // Check basic rules
        if let Some(denial) = self.check_basic_rules(client_id, tool_name, context, &permissions) {
            if let Permission::Deny(ref reason) = denial {
                warn!("Permission denied for {}: {}", client_id, reason);
                self.record_decision(false, Some(reason));
            }
            return Ok(denial);
        }

        // Check tool-specific rules
        if let Some(denial) = self.check_tool_rules(tool_name, context, &permissions) {
            if let Permission::Deny(ref reason) = denial {
                warn!("Permission denied for {}: {}", client_id, reason);
                self.record_decision(false, Some(reason));
            }
            return Ok(denial);
        }

        // Default allow
        debug!("Permission granted for {} to use {}", client_id, tool_name);
        self.record_decision(true, None);
        Ok(Permission::Allow)
    }

    async fn get_allowed_tools(&self, client_id: &str) -> Result<Vec<String>> {
        let permissions = self.get_client_permissions(client_id);

        if permissions.allowed_tools.is_empty() {
            // Return all tools not in deny lists
            let mut allowed = Vec::new();

            for tool_name in self.rules.tools.keys() {
                if !self.rules.global_deny_list.contains(tool_name)
                    && !permissions.denied_tools.contains(tool_name)
                {
                    allowed.push(tool_name.clone());
                }
            }

            Ok(allowed)
        } else {
            // Return explicit allow list
            Ok(permissions.allowed_tools.into_iter().collect())
        }
    }

    async fn update_permissions(
        &self,
        client_id: &str,
        permissions: ClientPermissions,
    ) -> Result<()> {
        let mut client_perms = self.client_permissions.write();
        client_perms.insert(client_id.to_string(), permissions);

        debug!("Updated permissions for client {}", client_id);
        Ok(())
    }

    fn get_stats(&self) -> PermissionStats {
        PermissionStats {
            total_checks: self.total_checks.load(Ordering::Relaxed),
            allowed: self.allowed_count.load(Ordering::Relaxed),
            denied: self.denied_count.load(Ordering::Relaxed),
            denied_by_reason: self.stats.denied_by_reason.clone(),
        }
    }
}