Merge branch 'main' of https://github.com/taylorwilsdon/google_workspace_mcp into fix/attachment-storage-absolute-path
This commit is contained in:
@@ -233,14 +233,18 @@ async def run_tool(server, tool_name: str, args: Dict[str, Any]) -> str:
|
||||
if fn is None:
|
||||
raise ValueError(f"Tool '{tool_name}' has no callable function")
|
||||
|
||||
logger.debug(f"[CLI] Executing tool: {tool_name} with args: {list(args.keys())}")
|
||||
call_args = dict(args)
|
||||
|
||||
try:
|
||||
logger.debug(
|
||||
f"[CLI] Executing tool: {tool_name} with args: {list(call_args.keys())}"
|
||||
)
|
||||
|
||||
# Call the tool function
|
||||
if asyncio.iscoroutinefunction(fn):
|
||||
result = await fn(**args)
|
||||
result = await fn(**call_args)
|
||||
else:
|
||||
result = fn(**args)
|
||||
result = fn(**call_args)
|
||||
|
||||
# Convert result to string if needed
|
||||
if isinstance(result, str):
|
||||
@@ -257,7 +261,7 @@ async def run_tool(server, tool_name: str, args: Dict[str, Any]) -> str:
|
||||
return (
|
||||
f"Error calling {tool_name}: {error_msg}\n\n"
|
||||
f"Required parameters: {required}\n"
|
||||
f"Provided parameters: {list(args.keys())}"
|
||||
f"Provided parameters: {list(call_args.keys())}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[CLI] Error executing {tool_name}: {e}", exc_info=True)
|
||||
|
||||
@@ -420,6 +420,7 @@ def get_auth_provider() -> Optional[GoogleProvider]:
|
||||
return _auth_provider
|
||||
|
||||
|
||||
@server.custom_route("/", methods=["GET"])
|
||||
@server.custom_route("/health", methods=["GET"])
|
||||
async def health_check(request: Request):
|
||||
try:
|
||||
@@ -482,7 +483,7 @@ async def legacy_oauth2_callback(request: Request) -> HTMLResponse:
|
||||
if error_message:
|
||||
return create_server_error_response(error_message)
|
||||
|
||||
logger.info(f"OAuth callback: Received code (state: {state}).")
|
||||
logger.info("OAuth callback: Received authorization code.")
|
||||
|
||||
mcp_session_id = None
|
||||
if hasattr(request, "state") and hasattr(request.state, "session_id"):
|
||||
|
||||
@@ -38,6 +38,7 @@ drive:
|
||||
- remove_drive_permission
|
||||
- transfer_drive_ownership
|
||||
- batch_share_drive_file
|
||||
- set_drive_file_permissions
|
||||
complete:
|
||||
- get_drive_file_permissions
|
||||
- check_drive_file_public_access
|
||||
@@ -85,6 +86,7 @@ sheets:
|
||||
extended:
|
||||
- list_spreadsheets
|
||||
- get_spreadsheet_info
|
||||
- format_sheet_range
|
||||
complete:
|
||||
- create_sheet
|
||||
- read_spreadsheet_comments
|
||||
|
||||
130
core/utils.py
130
core/utils.py
@@ -7,6 +7,7 @@ import ssl
|
||||
import asyncio
|
||||
import functools
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from googleapiclient.errors import HttpError
|
||||
@@ -29,6 +30,135 @@ class UserInputError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# Directories from which local file reads are allowed.
|
||||
# The user's home directory is the default safe base.
|
||||
# Override via ALLOWED_FILE_DIRS env var (os.pathsep-separated paths).
|
||||
_ALLOWED_FILE_DIRS_ENV = "ALLOWED_FILE_DIRS"
|
||||
|
||||
|
||||
def _get_allowed_file_dirs() -> list[Path]:
|
||||
"""Return the list of directories from which local file access is permitted."""
|
||||
env_val = os.environ.get(_ALLOWED_FILE_DIRS_ENV)
|
||||
if env_val:
|
||||
return [
|
||||
Path(p).expanduser().resolve()
|
||||
for p in env_val.split(os.pathsep)
|
||||
if p.strip()
|
||||
]
|
||||
home = Path.home()
|
||||
return [home] if home else []
|
||||
|
||||
|
||||
def validate_file_path(file_path: str) -> Path:
|
||||
"""
|
||||
Validate that a file path is safe to read from the server filesystem.
|
||||
|
||||
Resolves the path canonically (following symlinks), then verifies it falls
|
||||
within one of the allowed base directories. Rejects paths to sensitive
|
||||
system locations regardless of allowlist.
|
||||
|
||||
Args:
|
||||
file_path: The raw file path string to validate.
|
||||
|
||||
Returns:
|
||||
Path: The resolved, validated Path object.
|
||||
|
||||
Raises:
|
||||
ValueError: If the path is outside allowed directories or targets
|
||||
a sensitive location.
|
||||
"""
|
||||
resolved = Path(file_path).resolve()
|
||||
|
||||
if not resolved.exists():
|
||||
raise FileNotFoundError(f"Path does not exist: {resolved}")
|
||||
|
||||
# Block sensitive file patterns regardless of allowlist
|
||||
resolved_str = str(resolved)
|
||||
file_name = resolved.name.lower()
|
||||
|
||||
# Block .env files and variants (.env, .env.local, .env.production, etc.)
|
||||
if file_name == ".env" or file_name.startswith(".env."):
|
||||
raise ValueError(
|
||||
f"Access to '{resolved_str}' is not allowed: "
|
||||
".env files may contain secrets and cannot be read, uploaded, or attached."
|
||||
)
|
||||
|
||||
# Block well-known sensitive system paths (including macOS /private variants)
|
||||
sensitive_prefixes = (
|
||||
"/proc",
|
||||
"/sys",
|
||||
"/dev",
|
||||
"/etc/shadow",
|
||||
"/etc/passwd",
|
||||
"/private/etc/shadow",
|
||||
"/private/etc/passwd",
|
||||
)
|
||||
for prefix in sensitive_prefixes:
|
||||
if resolved_str == prefix or resolved_str.startswith(prefix + "/"):
|
||||
raise ValueError(
|
||||
f"Access to '{resolved_str}' is not allowed: "
|
||||
"path is in a restricted system location."
|
||||
)
|
||||
|
||||
# Block sensitive directories that commonly contain credentials/keys
|
||||
sensitive_dirs = (
|
||||
".ssh",
|
||||
".aws",
|
||||
".kube",
|
||||
".gnupg",
|
||||
".config/gcloud",
|
||||
)
|
||||
for sensitive_dir in sensitive_dirs:
|
||||
home = Path.home()
|
||||
blocked = home / sensitive_dir
|
||||
if resolved == blocked or str(resolved).startswith(str(blocked) + "/"):
|
||||
raise ValueError(
|
||||
f"Access to '{resolved_str}' is not allowed: "
|
||||
"path is in a directory that commonly contains secrets or credentials."
|
||||
)
|
||||
|
||||
# Block other credential/secret file patterns
|
||||
sensitive_names = {
|
||||
".credentials",
|
||||
".credentials.json",
|
||||
"credentials.json",
|
||||
"client_secret.json",
|
||||
"client_secrets.json",
|
||||
"service_account.json",
|
||||
"service-account.json",
|
||||
".npmrc",
|
||||
".pypirc",
|
||||
".netrc",
|
||||
".git-credentials",
|
||||
".docker/config.json",
|
||||
}
|
||||
if file_name in sensitive_names:
|
||||
raise ValueError(
|
||||
f"Access to '{resolved_str}' is not allowed: "
|
||||
"this file commonly contains secrets or credentials."
|
||||
)
|
||||
|
||||
allowed_dirs = _get_allowed_file_dirs()
|
||||
if not allowed_dirs:
|
||||
raise ValueError(
|
||||
"No allowed file directories configured. "
|
||||
"Set the ALLOWED_FILE_DIRS environment variable or ensure a home directory exists."
|
||||
)
|
||||
|
||||
for allowed in allowed_dirs:
|
||||
try:
|
||||
resolved.relative_to(allowed)
|
||||
return resolved
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
raise ValueError(
|
||||
f"Access to '{resolved_str}' is not allowed: "
|
||||
f"path is outside permitted directories ({', '.join(str(d) for d in allowed_dirs)}). "
|
||||
"Set ALLOWED_FILE_DIRS to adjust."
|
||||
)
|
||||
|
||||
|
||||
def check_credentials_directory_permissions(credentials_dir: str = None) -> None:
|
||||
"""
|
||||
Check if the service has appropriate permissions to create and write to the .credentials directory.
|
||||
|
||||
Reference in New Issue
Block a user