refactor(V2): audit.py usa actor:str invece di Principal, rimuovi legacy common/auth.py
- Eliminato src/cerbero_mcp/common/auth.py (V1 Principal/TokenStore/ACL) - audit_write_op: parametro principal:Principal → actor:str|None - mcp_bridge.py: TokenStore → valid_tokens:set[str] (V2 bearer model) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,6 @@ import os
|
|||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from cerbero_mcp.common.auth import Principal
|
|
||||||
from cerbero_mcp.common.logging import SecretsFilter, get_json_logger
|
from cerbero_mcp.common.logging import SecretsFilter, get_json_logger
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -67,7 +66,7 @@ def _configure_audit_sink() -> None:
|
|||||||
|
|
||||||
def audit_write_op(
|
def audit_write_op(
|
||||||
*,
|
*,
|
||||||
principal: Principal | None,
|
actor: str | None = None,
|
||||||
action: str,
|
action: str,
|
||||||
exchange: str,
|
exchange: str,
|
||||||
target: str | None = None,
|
target: str | None = None,
|
||||||
@@ -77,8 +76,8 @@ def audit_write_op(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Emit a structured audit log record per write operation.
|
"""Emit a structured audit log record per write operation.
|
||||||
|
|
||||||
principal: chi ha invocato (None se anonimo, ma normalmente _check
|
actor: identificatore di chi ha invocato (es. "testnet", "mainnet",
|
||||||
impedisce di arrivare qui senza principal).
|
oppure None per logging anonimo).
|
||||||
action: nome del tool (es. "place_order", "cancel_order").
|
action: nome del tool (es. "place_order", "cancel_order").
|
||||||
exchange: identificatore servizio (deribit, bybit, alpaca, hyperliquid).
|
exchange: identificatore servizio (deribit, bybit, alpaca, hyperliquid).
|
||||||
target: instrument/symbol/order_id su cui si agisce.
|
target: instrument/symbol/order_id su cui si agisce.
|
||||||
@@ -91,7 +90,7 @@ def audit_write_op(
|
|||||||
"audit_event": "write_op",
|
"audit_event": "write_op",
|
||||||
"action": action,
|
"action": action,
|
||||||
"exchange": exchange,
|
"exchange": exchange,
|
||||||
"principal": principal.name if principal else None,
|
"actor": actor,
|
||||||
"target": target,
|
"target": target,
|
||||||
"payload": payload or {},
|
"payload": payload or {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -28,8 +28,6 @@ import httpx
|
|||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from cerbero_mcp.common.auth import TokenStore
|
|
||||||
|
|
||||||
MCP_PROTOCOL_VERSION = "2024-11-05"
|
MCP_PROTOCOL_VERSION = "2024-11-05"
|
||||||
|
|
||||||
|
|
||||||
@@ -95,20 +93,22 @@ def mount_mcp_endpoint(
|
|||||||
*,
|
*,
|
||||||
name: str,
|
name: str,
|
||||||
version: str,
|
version: str,
|
||||||
token_store: TokenStore,
|
valid_tokens: set[str],
|
||||||
internal_base_url: str,
|
internal_base_url: str,
|
||||||
tools: list[dict],
|
tools: list[dict],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Registra un endpoint MCP JSON-RPC 2.0 su POST /mcp.
|
"""Registra un endpoint MCP JSON-RPC 2.0 su POST /mcp.
|
||||||
|
|
||||||
Ogni tool è proxato verso POST {internal_base_url}/tools/<name> con il
|
Ogni tool è proxato verso POST {internal_base_url}/tools/<name> 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:
|
Args:
|
||||||
app: istanza FastAPI del service
|
app: istanza FastAPI del service
|
||||||
name: nome server MCP
|
name: nome server MCP
|
||||||
version: versione del service
|
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")
|
internal_base_url: URL base interno (es. "http://localhost:9015")
|
||||||
tools: lista di {"name": str, "description": str, "input_schema"?: dict}
|
tools: lista di {"name": str, "description": str, "input_schema"?: dict}
|
||||||
"""
|
"""
|
||||||
@@ -207,8 +207,7 @@ def mount_mcp_endpoint(
|
|||||||
if not auth.startswith("Bearer "):
|
if not auth.startswith("Bearer "):
|
||||||
return JSONResponse({"error": "missing bearer token"}, status_code=401)
|
return JSONResponse({"error": "missing bearer token"}, status_code=401)
|
||||||
token = auth[len("Bearer "):].strip()
|
token = auth[len("Bearer "):].strip()
|
||||||
principal = token_store.get(token)
|
if token not in valid_tokens:
|
||||||
if principal is None:
|
|
||||||
return JSONResponse({"error": "invalid token"}, status_code=403)
|
return JSONResponse({"error": "invalid token"}, status_code=403)
|
||||||
|
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
|
|||||||
Reference in New Issue
Block a user