Develop #16

Merged
tao.chen merged 273 commits from develop into main 2026-08-21 10:42:09 +08:00
Showing only changes of commit d4013a07a9 - Show all commits
+21 -29
View File
@@ -1,19 +1,16 @@
from __future__ import annotations
import re import re
from typing import Optional from typing import Optional
from common.auth.jwt import JwtError, verify_jwt_token
from common.auth.membership import MembershipError, load_active_membership
from common.db.models import Scripts
from fastapi import APIRouter, Depends, HTTPException, Request, Response from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from backend.dependencies import database_session from backend.dependencies import database_session
from backend.runtime_client import RuntimeClientError from backend.runtime_client import RuntimeClientError
from common.auth.jwt import JwtError, verify_jwt_token
from common.auth.membership import MembershipError, load_active_membership
from common.db.models import Scripts
router = APIRouter(tags=["jupyter"]) router = APIRouter(tags=["jupyter"])
security = HTTPBearer(auto_error=False) security = HTTPBearer(auto_error=False)
@@ -25,19 +22,14 @@ def extract_notebook_path(
) -> Optional[str]: ) -> Optional[str]:
"""Pull the relative notebook path out of the original request URI. """Pull the relative notebook path out of the original request URI.
Match ``/jupyter/{workspace_id}/notebooks/*.ipynb`` (the legacy Only ``/jupyter/{workspace_id}/notebooks/*.ipynb`` requests are
iframe flow) and ``/jupyter/{workspace_id}/api/contents/*.ipynb`` subject to file-level lock checks; everything else (tree views,
(the Monaco-based Contents API PUT path). The caller decides ``/api/contents`` and WebSocket upgrades) bypasses the lock.
whether to actually enforce the lock — typically only on writes.
""" """
nb_pattern = rf"^/jupyter/{re.escape(workspace_id)}/notebooks/(.+\.ipynb)" pattern = rf"^/jupyter/{re.escape(workspace_id)}/notebooks/(.+\.ipynb)"
contents_pattern = ( match = re.match(pattern, uri)
rf"^/jupyter/{re.escape(workspace_id)}/api/contents/(.+\.ipynb)" if match:
) return match.group(1)
for pattern in (nb_pattern, contents_pattern):
match = re.match(pattern, uri)
if match:
return match.group(1)
return None return None
@@ -131,16 +123,16 @@ async def verify_jupyter_access(
await load_active_membership_or_403(session, user_id, workspace_id) await load_active_membership_or_403(session, user_id, workspace_id)
notebook_path = extract_notebook_path(original_uri, workspace_id) notebook_path = extract_notebook_path(original_uri, workspace_id)
# Lock only on writes — reads (GET) stay open so anyone can still if notebook_path and await check_notebook_is_locked(
# preview a locked notebook the way they could via the iframe before. session,
if notebook_path and request.method in {"PUT", "POST", "PATCH", "DELETE"}: workspace_id,
if await check_notebook_is_locked( notebook_path,
session, workspace_id, notebook_path, user_id, user_id,
): ):
raise HTTPException( raise HTTPException(
status_code=403, status_code=403,
detail=f"Notebook '{notebook_path}' is currently locked", detail=f"Notebook '{notebook_path}' is currently locked",
) )
runtime_client = request.app.state.runtime_client runtime_client = request.app.state.runtime_client
ws_info = await runtime_client.get_workspace(workspace_id) ws_info = await runtime_client.get_workspace(workspace_id)