Prevent stale escape errors from collapsing live browse grants
This commit is contained in:
parent
58b361b811
commit
d20e9dd852
4 changed files with 35 additions and 25 deletions
|
|
@ -6614,6 +6614,7 @@ from api.workspace import (
|
|||
set_last_workspace,
|
||||
git_info_for_workspace,
|
||||
authorize_escape_target,
|
||||
EscapeAuthorizationExpiredError,
|
||||
list_dir,
|
||||
list_authorized_escape_dir,
|
||||
dir_signature,
|
||||
|
|
@ -9825,9 +9826,6 @@ def handle_get(handler, parsed) -> bool:
|
|||
if parsed.path == "/api/list":
|
||||
return _handle_list_dir(handler, parsed)
|
||||
|
||||
if parsed.path == "/api/escape/authorize":
|
||||
return _handle_escape_authorize(handler, parsed)
|
||||
|
||||
if parsed.path == "/api/escape/list":
|
||||
return _handle_escape_list_dir(handler, parsed)
|
||||
|
||||
|
|
@ -12995,10 +12993,10 @@ def _handle_escape_list_dir(handler, parsed):
|
|||
return j(handler, payload)
|
||||
except FileNotFoundError as exc:
|
||||
return bad(handler, _sanitize_error(exc), 404)
|
||||
except EscapeAuthorizationExpiredError as exc:
|
||||
return bad(handler, _sanitize_error(exc), 403)
|
||||
except ValueError as exc:
|
||||
msg = _sanitize_error(exc)
|
||||
status = 403 if "expired" in msg.lower() else 404
|
||||
return bad(handler, msg, status)
|
||||
return bad(handler, _sanitize_error(exc), 404)
|
||||
|
||||
|
||||
def _handle_escape_file_read(handler, parsed):
|
||||
|
|
@ -13018,10 +13016,10 @@ def _handle_escape_file_read(handler, parsed):
|
|||
return j(handler, read_authorized_escape_file_content(Path(s.workspace), sid, token, rel))
|
||||
except FileNotFoundError as exc:
|
||||
return bad(handler, _sanitize_error(exc), 404)
|
||||
except EscapeAuthorizationExpiredError as exc:
|
||||
return bad(handler, _sanitize_error(exc), 403)
|
||||
except ValueError as exc:
|
||||
msg = _sanitize_error(exc)
|
||||
status = 403 if "expired" in msg.lower() else 404
|
||||
return bad(handler, msg, status)
|
||||
return bad(handler, _sanitize_error(exc), 404)
|
||||
|
||||
|
||||
def _handle_escape_file_raw(handler, parsed):
|
||||
|
|
@ -13042,10 +13040,10 @@ def _handle_escape_file_raw(handler, parsed):
|
|||
anchor_root, target = raw_authorized_escape_target(Path(s.workspace), sid, token, rel)
|
||||
except FileNotFoundError:
|
||||
return j(handler, {"error": "not found"}, status=404)
|
||||
except EscapeAuthorizationExpiredError as exc:
|
||||
return bad(handler, _sanitize_error(exc), 403)
|
||||
except ValueError as exc:
|
||||
msg = _sanitize_error(exc)
|
||||
status = 403 if "expired" in msg.lower() else 404
|
||||
return bad(handler, msg, status)
|
||||
return bad(handler, _sanitize_error(exc), 404)
|
||||
if not target.exists() or not target.is_file():
|
||||
return j(handler, {"error": "not found"}, status=404)
|
||||
ext = target.suffix.lower()
|
||||
|
|
|
|||
|
|
@ -1461,7 +1461,13 @@ def _escape_virtual_path(root: str, rel: str) -> str:
|
|||
def _escape_surface_target(workspace: Path, rel: str) -> tuple[Path, Path]:
|
||||
workspace_root = workspace.resolve()
|
||||
surface_rel = _normalize_workspace_rel_path(rel)
|
||||
surface_path = workspace / surface_rel
|
||||
surface_posix = PurePosixPath(surface_rel)
|
||||
parent_rel = str(surface_posix.parent)
|
||||
if parent_rel in ("", "."):
|
||||
parent_path = workspace_root
|
||||
else:
|
||||
parent_path = safe_resolve_ws(workspace_root, parent_rel)
|
||||
surface_path = parent_path / surface_posix.name
|
||||
if not surface_path.is_symlink():
|
||||
raise ValueError(f"Path is not an escape-target symlink: {rel}")
|
||||
target = surface_path.resolve()
|
||||
|
|
@ -1485,6 +1491,10 @@ def _escape_authorized_root(target: Path) -> tuple[Path, str]:
|
|||
return resolved_target.parent, resolved_target.name
|
||||
|
||||
|
||||
class EscapeAuthorizationExpiredError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _escape_prune_tokens(now: float | None = None) -> None:
|
||||
cutoff = time.time() if now is None else now
|
||||
expired = [token for token, record in _ESCAPE_AUTH_TOKENS.items() if float(record.get("expires_at") or 0.0) <= cutoff]
|
||||
|
|
@ -1531,25 +1541,21 @@ def _escape_authorization_record(workspace: Path, session_id: str, token: str) -
|
|||
_escape_prune_tokens(now)
|
||||
record = dict(_ESCAPE_AUTH_TOKENS.get(token) or {})
|
||||
if not record:
|
||||
raise ValueError("Escape authorization expired")
|
||||
raise EscapeAuthorizationExpiredError("Escape authorization expired")
|
||||
if str(record.get("session_id") or "") != str(session_id or ""):
|
||||
raise ValueError("Escape authorization expired")
|
||||
raise EscapeAuthorizationExpiredError("Escape authorization expired")
|
||||
if str(record.get("workspace_root") or "") != workspace_root:
|
||||
raise ValueError("Escape authorization expired")
|
||||
if now >= float(record.get("expires_at") or 0.0):
|
||||
with _ESCAPE_AUTH_LOCK:
|
||||
_ESCAPE_AUTH_TOKENS.pop(token, None)
|
||||
raise ValueError("Escape authorization expired")
|
||||
raise EscapeAuthorizationExpiredError("Escape authorization expired")
|
||||
surface_path = _normalize_workspace_rel_path(record.get("surface_path") or ".")
|
||||
surface_target = str(record.get("surface_target") or "")
|
||||
try:
|
||||
_surface, current_target = _escape_surface_target(Path(workspace_root), surface_path)
|
||||
except ValueError:
|
||||
raise ValueError("Escape authorization expired") from None
|
||||
raise EscapeAuthorizationExpiredError("Escape authorization expired") from None
|
||||
if str(current_target.resolve()) != surface_target:
|
||||
raise ValueError("Escape authorization expired")
|
||||
raise EscapeAuthorizationExpiredError("Escape authorization expired")
|
||||
if not current_target.exists() or _is_blocked_system_path(current_target):
|
||||
raise ValueError("Escape authorization expired")
|
||||
raise EscapeAuthorizationExpiredError("Escape authorization expired")
|
||||
return record
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -697,7 +697,7 @@ async function loadDir(path, opts={}){
|
|||
if(!path||path==='.') _refreshGitBadge();
|
||||
}catch(e){
|
||||
const grant = _workspaceEscapeGrantForPath(path);
|
||||
if(grant && (e && (e.status === 403 || e.status === 404))){
|
||||
if(grant && e && e.status===403){
|
||||
_clearWorkspaceEscapeGrant(grant.path);
|
||||
showToast(t('external_link_grant_expired') || t('file_open_failed'), 5000, 'error');
|
||||
return;
|
||||
|
|
@ -1095,7 +1095,7 @@ async function openFile(path, opts={}){
|
|||
renderCodePreviewContent(path, data.content);
|
||||
}catch(e){
|
||||
const grant = _workspaceEscapeGrantForPath(path);
|
||||
if(grant && (e && (e.status === 403 || e.status === 404))){
|
||||
if(grant && e && e.status===403){
|
||||
_clearWorkspaceEscapeGrant(grant.path);
|
||||
showToast(t('external_link_grant_expired') || t('file_open_failed'), 5000, 'error');
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -176,6 +176,12 @@ class TestIssue4582EscapeNavigationLive:
|
|||
auth, status = _post_json("/api/escape/authorize", {"session_id": sid, "path": "escape"})
|
||||
assert status == 200, auth
|
||||
|
||||
nested_auth, nested_status = _post_json(
|
||||
"/api/escape/authorize",
|
||||
{"session_id": sid, "path": "escape/nested-escape"},
|
||||
)
|
||||
assert nested_status in (403, 404), nested_auth
|
||||
|
||||
listed = _get_json(
|
||||
f"/api/escape/list?session_id={sid}&token={auth['token']}&path=escape"
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue