rollback
This commit is contained in:
@@ -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,16 +22,11 @@ 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 = (
|
|
||||||
rf"^/jupyter/{re.escape(workspace_id)}/api/contents/(.+\.ipynb)"
|
|
||||||
)
|
|
||||||
for pattern in (nb_pattern, contents_pattern):
|
|
||||||
match = re.match(pattern, uri)
|
match = re.match(pattern, uri)
|
||||||
if match:
|
if match:
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
@@ -131,11 +123,11 @@ 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user