Files
ft/Client/Packages/io.github.hatayama.uloopmcp/Editor/Security/McpSecurityChecker.cs

293 lines
11 KiB
C#

using System;
using System.Reflection;
using UnityEngine;
namespace io.github.hatayama.uLoopMCP
{
/// <summary>
/// Security checker for Unity MCP tools - provides runtime tool blocking based on user settings
///
/// Design document reference: Packages/src/Editor/Security/SECURITY.md
///
/// Related classes:
/// - ULoopSettings: Persistent security settings storage (.uloop/settings.permissions.json)
/// - McpSecurityException: Custom exception for security violations
/// - UnityToolRegistry: Tool registration and execution
/// - McpBridgeServer: Server that executes tools
/// </summary>
public static class McpSecurityChecker
{
/// <summary>
/// Checks if a tool is allowed based on current security settings
/// </summary>
/// <param name="toolName">The name of the tool to check</param>
/// <returns>True if tool is allowed, false otherwise</returns>
public static bool IsToolAllowed(string toolName)
{
if (string.IsNullOrEmpty(toolName))
{
return false;
}
// Get tool info from registry
var toolInfo = GetToolSecurityInfoFromRegistry(toolName);
if (!toolInfo.HasValue)
{
// Unknown tool - default to block for security
return false;
}
return IsToolAllowedByAttribute(toolInfo.Value);
}
/// <summary>
/// Checks if a command is allowed based on current security settings
/// </summary>
/// <param name="commandName">The name of the command to check</param>
/// <returns>True if command is allowed, false otherwise</returns>
[System.Obsolete("Use IsToolAllowed instead. This method will be removed in a future version.")]
public static bool IsCommandAllowed(string commandName)
{
return IsToolAllowed(commandName);
}
/// <summary>
/// Gets tool security info from the tool registry
/// </summary>
/// <param name="toolName">The name of the tool</param>
/// <returns>Tool security info or null if not found</returns>
private static ToolAttributeInfo? GetToolSecurityInfoFromRegistry(string toolName)
{
// Get the tool registry instance
var registry = CustomToolManager.GetRegistry();
if (registry == null)
{
return null;
}
// Find the tool and get its attribute
var toolType = registry.GetToolType(toolName);
if (toolType == null)
{
return null;
}
var attribute = toolType.GetCustomAttribute<McpToolAttribute>();
if (attribute == null)
{
return new ToolAttributeInfo(toolName, SecuritySettings.None);
}
return new ToolAttributeInfo(toolName, attribute.RequiredSecuritySetting);
}
/// <summary>
/// Checks if tool is allowed based on its attribute settings
/// </summary>
/// <param name="toolInfo">Tool attribute information</param>
/// <returns>True if tool is allowed</returns>
private static bool IsToolAllowedByAttribute(ToolAttributeInfo toolInfo)
{
switch (toolInfo.RequiredSecuritySetting)
{
case SecuritySettings.None:
return IsThirdPartyToolAllowed(toolInfo.ToolName);
default:
return false; // Unknown setting - block by default
}
}
/// <summary>
/// Validates tool execution and throws exception if blocked
/// </summary>
/// <param name="toolName">The name of the tool to validate</param>
/// <param name="throwOnBlock">If true, throws exception when blocked. If false, returns validation result.</param>
/// <returns>True if tool is allowed</returns>
/// <exception cref="McpSecurityException">Thrown when tool is blocked and throwOnBlock is true</exception>
public static bool ValidateTool(string toolName, bool throwOnBlock = true)
{
if (IsToolAllowed(toolName))
{
return true;
}
string reason = GetBlockReason(toolName);
if (throwOnBlock)
{
throw new McpSecurityException(toolName, reason);
}
return false;
}
/// <summary>
/// Validates command execution and throws exception if blocked
/// </summary>
/// <param name="commandName">The name of the command to validate</param>
/// <param name="throwOnBlock">If true, throws exception when blocked. If false, returns validation result.</param>
/// <returns>True if command is allowed</returns>
/// <exception cref="McpSecurityException">Thrown when command is blocked and throwOnBlock is true</exception>
[System.Obsolete("Use ValidateTool instead. This method will be removed in a future version.")]
public static bool ValidateCommand(string commandName, bool throwOnBlock = true)
{
return ValidateTool(commandName, throwOnBlock);
}
/// <summary>
/// Gets the user-friendly reason why a tool is blocked
/// </summary>
/// <param name="toolName">The name of the tool</param>
/// <returns>Human-readable reason string</returns>
public static string GetBlockReason(string toolName)
{
var toolInfo = GetToolSecurityInfoFromRegistry(toolName);
if (!toolInfo.HasValue)
{
return $"Tool '{toolName}' is not allowed by security policy.";
}
switch (toolInfo.Value.RequiredSecuritySetting)
{
case SecuritySettings.None:
if (IsThirdPartyTool(toolName))
{
return "Third party tools execution is disabled. Enable 'Allow Third Party Tools' in uLoopMCP Security Settings.";
}
return $"Tool '{toolName}' is not allowed by security policy.";
default:
return $"Tool '{toolName}' is not allowed by security policy.";
}
}
/// <summary>
/// Gets security information for a tool
/// </summary>
/// <param name="toolName">The name of the tool</param>
/// <returns>Security information object</returns>
public static ToolSecurityInfo GetToolSecurityInfo(string toolName)
{
bool isAllowed = IsToolAllowed(toolName);
string reason = isAllowed ? "Tool is allowed" : GetBlockReason(toolName);
return new ToolSecurityInfo(toolName, isAllowed, reason);
}
/// <summary>
/// Checks if third party tools execution is allowed
/// </summary>
/// <returns>True if third party tools execution is allowed</returns>
private static bool IsThirdPartyToolsAllowed()
{
return ULoopSettings.GetAllowThirdPartyTools();
}
/// <summary>
/// Checks if a specific tool is allowed (considers both explicit setting and third party status)
/// </summary>
/// <param name="toolName">The name of the tool to check</param>
/// <returns>True if tool is allowed</returns>
private static bool IsThirdPartyToolAllowed(string toolName)
{
// If it's not a third party tool (i.e., official tool), allow it
if (!IsThirdPartyTool(toolName))
{
return true;
}
// If it's a third party tool, check the setting
return IsThirdPartyToolsAllowed();
}
/// <summary>
/// Checks if a tool is a third party tool based on its assembly
/// </summary>
/// <param name="toolName">The name of the tool to check</param>
/// <returns>True if tool is from a third party assembly</returns>
private static bool IsThirdPartyTool(string toolName)
{
// Get tool type from registry
var registry = CustomToolManager.GetRegistry();
if (registry == null)
{
return true; // Default to third party if registry unavailable
}
var toolType = registry.GetToolType(toolName);
if (toolType == null)
{
return true; // Default to third party if tool type not found
}
// Check assembly name
string assemblyName = toolType.Assembly.GetName().Name;
// Official uLoopMCP assembly
return assemblyName != "uLoopMCP.Editor";
}
}
/// <summary>
/// Security information for a tool
/// </summary>
public readonly struct ToolSecurityInfo
{
public readonly string ToolName;
public readonly bool IsAllowed;
public readonly string Reason;
public ToolSecurityInfo(string toolName, bool isAllowed, string reason)
{
ToolName = toolName;
IsAllowed = isAllowed;
Reason = reason;
}
}
/// <summary>
/// Tool attribute information for security checking
/// </summary>
internal readonly struct ToolAttributeInfo
{
public readonly string ToolName;
public readonly SecuritySettings RequiredSecuritySetting;
public ToolAttributeInfo(string toolName, SecuritySettings requiredSecuritySetting)
{
ToolName = toolName;
RequiredSecuritySetting = requiredSecuritySetting;
}
}
/// <summary>
/// Exception thrown when a tool is blocked by security settings
/// </summary>
public class McpSecurityException : Exception
{
public string ToolName { get; }
public string SecurityReason { get; }
/// <summary>
/// Backwards compatibility property for CommandName
/// </summary>
[System.Obsolete("Use ToolName instead. This property will be removed in a future version.")]
public string CommandName => ToolName;
public McpSecurityException(string toolName, string reason)
: base($"Tool '{toolName}' is blocked by security settings: {reason}")
{
ToolName = toolName;
SecurityReason = reason;
}
public McpSecurityException(string toolName, string reason, Exception innerException)
: base($"Tool '{toolName}' is blocked by security settings: {reason}", innerException)
{
ToolName = toolName;
SecurityReason = reason;
}
}
}