mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
fix: replace Bearer JWTs with path-bound access tickets in MCP download tools
link_capture_download and get_symbol embedded a 10-min JWT in the Authorization header of the curl command they return; LLM clients retyping that command corrupted the long token — the same failure class as the console WebSocket URLs fixed in the previous commit. Generalize the ticket store (console_tickets.py -> access_tickets.py, ConsoleTicketService -> AccessTicketService): a ticket now binds to either a node's console endpoints (WebSocket, matched against route path params) or one exact REST resource path (capture file, symbol image). The two binding modes are isolated — a node-bound ticket cannot authenticate a REST resource and vice versa. get_user_from_token redeems path-bound tickets through the existing token parameter / Bearer header, matched exactly against request.url.path, then reuses the shared user lookup and token_version revocation checks. Download URLs embed ?token=<ticket> and the curl commands no longer carry a Bearer header.
This commit is contained in:
8 files changed
+293
-88
No files matched your search
@@ -30,8 +30,8 @@ from gns3server.db.repositories.api_keys import ApiKeysRepository
|
||||
from gns3server.db.repositories.users import UsersRepository
|
||||
from gns3server.db.repositories.rbac import RbacRepository
|
||||
from gns3server.schemas.controller.tokens import TokenData
|
||||
from gns3server.services import auth_service, console_ticket_service
|
||||
from gns3server.services.console_tickets import TICKET_PREFIX
|
||||
from gns3server.services import auth_service, access_ticket_service
|
||||
from gns3server.services.access_tickets import TICKET_PREFIX
|
||||
from .database import get_repository
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -50,6 +50,7 @@ def _reject_refresh_token(token_data) -> None:
|
||||
|
||||
|
||||
async def get_user_from_token(
|
||||
request: Request,
|
||||
bearer_token: str = Depends(oauth2_scheme),
|
||||
user_repo: UsersRepository = Depends(get_repository(UsersRepository)),
|
||||
api_keys_repo: ApiKeysRepository = Depends(get_repository(ApiKeysRepository)),
|
||||
@@ -68,6 +69,38 @@ async def get_user_from_token(
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if token.startswith(TICKET_PREFIX):
|
||||
# Access tickets ("gns3t_…"): short-lived credentials bound to one
|
||||
# exact resource path, minted by the MCP download tools (capture
|
||||
# files, symbols). redeem_for_path() confines the ticket to that
|
||||
# path, so it cannot be replayed against any other resource.
|
||||
ticket = access_ticket_service.redeem_for_path(token, request.url.path)
|
||||
if ticket is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid or expired access ticket",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
token_data = TokenData(
|
||||
username=ticket.username,
|
||||
token_version=ticket.token_version,
|
||||
token_use="access",
|
||||
)
|
||||
user = await user_repo.get_user_by_username(token_data.username)
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
if token_data.token_version != user.token_version:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"Token has been revoked for '{token_data.username}'",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return user
|
||||
|
||||
# API Key authentication — format: gns3_<api_key_id>_<random_secret>
|
||||
# Direct lookup by UUID avoids O(n) scan of all keys.
|
||||
if token.startswith("gns3_"):
|
||||
@@ -152,12 +185,12 @@ async def get_current_active_user_from_websocket(
|
||||
|
||||
try:
|
||||
if token.startswith(TICKET_PREFIX):
|
||||
# Console tickets ("gns3t_…"): short-lived credentials bound to one
|
||||
# node's console endpoints, minted by the node_console MCP tool.
|
||||
# redeem() confines them to the route matching their binding, so
|
||||
# they cannot authenticate the notification/wireshark sockets that
|
||||
# share this dependency.
|
||||
ticket = console_ticket_service.redeem(token, websocket.path_params)
|
||||
# Node-bound access tickets ("gns3t_…"): short-lived credentials
|
||||
# for one node's console endpoints, minted by the node_console
|
||||
# MCP tool. redeem() confines them to the route matching their
|
||||
# binding, so they cannot authenticate the notification/wireshark
|
||||
# sockets that share this dependency.
|
||||
ticket = access_ticket_service.redeem(token, websocket.path_params)
|
||||
if ticket is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
|
||||
Reference in new issue
Block a user