diff --git a/src/cerbero_mcp/common/audit.py b/src/cerbero_mcp/common/audit.py index 3e8669c..427f050 100644 --- a/src/cerbero_mcp/common/audit.py +++ b/src/cerbero_mcp/common/audit.py @@ -23,7 +23,6 @@ import os from logging.handlers import TimedRotatingFileHandler from typing import Any -from cerbero_mcp.common.auth import Principal from cerbero_mcp.common.logging import SecretsFilter, get_json_logger try: @@ -67,7 +66,7 @@ def _configure_audit_sink() -> None: def audit_write_op( *, - principal: Principal | None, + actor: str | None = None, action: str, exchange: str, target: str | None = None, @@ -77,8 +76,8 @@ def audit_write_op( ) -> None: """Emit a structured audit log record per write operation. - principal: chi ha invocato (None se anonimo, ma normalmente _check - impedisce di arrivare qui senza principal). + actor: identificatore di chi ha invocato (es. "testnet", "mainnet", + oppure None per logging anonimo). action: nome del tool (es. "place_order", "cancel_order"). exchange: identificatore servizio (deribit, bybit, alpaca, hyperliquid). target: instrument/symbol/order_id su cui si agisce. @@ -91,7 +90,7 @@ def audit_write_op( "audit_event": "write_op", "action": action, "exchange": exchange, - "principal": principal.name if principal else None, + "actor": actor, "target": target, "payload": payload or {}, } diff --git a/src/cerbero_mcp/common/auth.py b/src/cerbero_mcp/common/auth.py deleted file mode 100644 index d791527..0000000 --- a/src/cerbero_mcp/common/auth.py +++ /dev/null @@ -1,98 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass, field -from functools import wraps - -from fastapi import HTTPException, Request, status - - -@dataclass -class Principal: - name: str - capabilities: set[str] = field(default_factory=set) - - -@dataclass -class TokenStore: - tokens: dict[str, Principal] - - def get(self, token: str) -> Principal | None: - return self.tokens.get(token) - - -def require_principal(request: Request) -> Principal: - auth = request.headers.get("Authorization", "") - if not auth.startswith("Bearer "): - raise HTTPException(status.HTTP_401_UNAUTHORIZED, "missing bearer token") - token = auth[len("Bearer "):].strip() - store: TokenStore = request.app.state.token_store - principal = store.get(token) - if principal is None: - raise HTTPException(status.HTTP_403_FORBIDDEN, "invalid token") - return principal - - -def acl_requires(*, core: bool = False, observer: bool = False) -> Callable: - """Decorator: require at least one matching capability.""" - allowed: set[str] = set() - if core: - allowed.add("core") - if observer: - allowed.add("observer") - - def decorator(func: Callable) -> Callable: - @wraps(func) - async def async_wrapper(*args, **kwargs): - principal = kwargs.get("principal") - if principal is None: - for a in args: - if isinstance(a, Principal): - principal = a - break - if principal is None or not (principal.capabilities & allowed): - raise HTTPException( - status.HTTP_403_FORBIDDEN, - f"capability required: {allowed}", - ) - return await func(*args, **kwargs) if _is_coro(func) else func(*args, **kwargs) - - @wraps(func) - def sync_wrapper(*args, **kwargs): - principal = kwargs.get("principal") - if principal is None: - for a in args: - if isinstance(a, Principal): - principal = a - break - if principal is None or not (principal.capabilities & allowed): - raise HTTPException( - status.HTTP_403_FORBIDDEN, - f"capability required: {allowed}", - ) - return func(*args, **kwargs) - - return async_wrapper if _is_coro(func) else sync_wrapper - - return decorator - - -def _is_coro(func: Callable) -> bool: - import asyncio - return asyncio.iscoroutinefunction(func) - - -def load_token_store_from_files( - core_token_file: str | None, - observer_token_file: str | None, -) -> TokenStore: - tokens: dict[str, Principal] = {} - if core_token_file: - with open(core_token_file) as f: - tokens[f.read().strip()] = Principal(name="core", capabilities={"core"}) - if observer_token_file: - with open(observer_token_file) as f: - tokens[f.read().strip()] = Principal( - name="observer", capabilities={"observer"} - ) - return TokenStore(tokens=tokens) diff --git a/src/cerbero_mcp/common/mcp_bridge.py b/src/cerbero_mcp/common/mcp_bridge.py index abd1e98..a22be44 100644 --- a/src/cerbero_mcp/common/mcp_bridge.py +++ b/src/cerbero_mcp/common/mcp_bridge.py @@ -28,8 +28,6 @@ import httpx from fastapi import FastAPI, Request from fastapi.responses import JSONResponse -from cerbero_mcp.common.auth import TokenStore - MCP_PROTOCOL_VERSION = "2024-11-05" @@ -95,20 +93,22 @@ def mount_mcp_endpoint( *, name: str, version: str, - token_store: TokenStore, + valid_tokens: set[str], internal_base_url: str, tools: list[dict], ) -> None: """Registra un endpoint MCP JSON-RPC 2.0 su POST /mcp. Ogni tool è proxato verso POST {internal_base_url}/tools/ con il - Bearer token del client MCP (preservando le ACL REST esistenti). + Bearer token del client MCP. L'auth è già gestita dal middleware V2 + (bearer testnet/mainnet); qui si ricontrolla che il token sia nei + valid_tokens prima di proxare. Args: app: istanza FastAPI del service name: nome server MCP version: versione del service - token_store: lo stesso usato dai tool REST + valid_tokens: set di token validi (testnet + mainnet) internal_base_url: URL base interno (es. "http://localhost:9015") tools: lista di {"name": str, "description": str, "input_schema"?: dict} """ @@ -207,8 +207,7 @@ def mount_mcp_endpoint( if not auth.startswith("Bearer "): return JSONResponse({"error": "missing bearer token"}, status_code=401) token = auth[len("Bearer "):].strip() - principal = token_store.get(token) - if principal is None: + if token not in valid_tokens: return JSONResponse({"error": "invalid token"}, status_code=403) body = await request.json()