feat: rebuild shared projects with locks and git activity history#3
Conversation
Replace plain-text loading indicators in Database, Design, Production, and project-fallback panes with a shared PaneLoading component featuring a centered Loader2 spinner and animated message text.
| if str(e) == "not_found" | ||
| else 403 | ||
| ) | ||
| return JSONResponse({"error": str(e)}, status_code=code) |
Check warning
Code scanning / CodeQL
Information exposure through an exception Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
In general, to fix this kind of issue, avoid returning exception messages or stack traces directly in HTTP responses. Instead, log the detailed error server-side (if needed) and return a generic or controlled error code/message to the client.
For this specific case, we should stop sending str(e) back to the user and instead send a fixed error identifier derived from the same information currently used to compute the status code. The rest of the behavior (which status code is returned) can stay unchanged, preserving functionality from the client’s perspective while removing the information exposure.
Concretely, in src/runtimes/ws_server.py:
- In
api_project_members_add, replace{"error": str(e)}with a small set of fixed tokens based on the same string that is currently used ("not_authenticated","not_found", or"forbidden"). - In
api_project_members_remove_by_sub, apply the same pattern to theexcept PermissionError as e:block. - Optionally, we can log the original exception message with the
loggingmodule (already imported aslogging) for debugging, but avoid including it in the HTTP response.
No new imports or helper methods are strictly necessary; we can implement the mapping inline where the JSONResponse is constructed.
| @@ -1135,14 +1135,20 @@ | ||
| p = await _ensure_project_owner_access_async(request, project_id=project_id) | ||
| actor = _get_actor_from_request(request) | ||
| except PermissionError as e: | ||
| error_str = str(e) | ||
| code = ( | ||
| 401 | ||
| if str(e) == "not_authenticated" | ||
| if error_str == "not_authenticated" | ||
| else 404 | ||
| if str(e) == "not_found" | ||
| if error_str == "not_found" | ||
| else 403 | ||
| ) | ||
| return JSONResponse({"error": str(e)}, status_code=code) | ||
| # Do not expose internal exception messages to the client. | ||
| if error_str in ("not_authenticated", "not_found"): | ||
| error_code = error_str | ||
| else: | ||
| error_code = "forbidden" | ||
| return JSONResponse({"error": error_code}, status_code=code) | ||
|
|
||
| from src.db.provisioning import hasura_client_from_env | ||
| from src.projects.store import add_project_member | ||
| @@ -1185,14 +1186,20 @@ | ||
| try: | ||
| p = await _ensure_project_owner_access_async(request, project_id=project_id) | ||
| except PermissionError as e: | ||
| error_str = str(e) | ||
| code = ( | ||
| 401 | ||
| if str(e) == "not_authenticated" | ||
| if error_str == "not_authenticated" | ||
| else 404 | ||
| if str(e) == "not_found" | ||
| if error_str == "not_found" | ||
| else 403 | ||
| ) | ||
| return JSONResponse({"error": str(e)}, status_code=code) | ||
| # Do not expose internal exception messages to the client. | ||
| if error_str in ("not_authenticated", "not_found"): | ||
| error_code = error_str | ||
| else: | ||
| error_code = "forbidden" | ||
| return JSONResponse({"error": error_code}, status_code=code) | ||
| if target_sub == str(getattr(p, "owner_sub", "") or "").strip(): | ||
| return JSONResponse({"error": "cannot_remove_owner"}, status_code=400) | ||
|
|
| if str(e) == "not_found" | ||
| else 403 | ||
| ) | ||
| return JSONResponse({"error": str(e)}, status_code=code) |
Check warning
Code scanning / CodeQL
Information exposure through an exception Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
In general, to fix this problem you should avoid returning raw exception messages or stack traces directly to users. Instead, map exceptions to controlled, stable error codes or messages, and log the detailed exception (including stack trace) on the server side for diagnostics. This ensures that clients receive only intended information, while developers retain full context via logs.
For this specific endpoint in src/runtimes/ws_server.py, we already derive an HTTP status code from the PermissionError’s string value, and we appear to be using constrained values like "not_authenticated" and "not_found". The safest way to preserve existing behavior while eliminating the information-exposure pattern is:
- Replace
{"error": str(e)}with a sanitized, controlled error code that does not rely on arbitrary exception messages. - Since the code already derives
codefromstr(e), we can preserve the same categorization, but we should no longer echostr(e)itself back to the client. Instead, we can:- Map
"not_authenticated"→"not_authenticated", "not_found"→"not_found",- any other case →
"forbidden".
- Map
- Optionally, to aid debugging without changing imports, we can log the exception using the existing
loggingmodule already imported at the top of the file, but only on the server side and not in the response body.
Concretely, in api_project_members_remove_by_email around lines 1221–1231:
- Keep the
try/except PermissionError as estructure. - Inside the
except, keep thecodecomputation, but add a small mapping to a safeerror_codestring that does not depend directly onstr(e)for the client response. - Optionally log the exception via
logging.exception("...")orlogging.warning(...).
This requires no new imports or external dependencies, as logging is already imported.
| @@ -1228,7 +1228,16 @@ | ||
| if str(e) == "not_found" | ||
| else 403 | ||
| ) | ||
| return JSONResponse({"error": str(e)}, status_code=code) | ||
| # Log the detailed exception server-side, but do not expose it to the client. | ||
| logging.warning("PermissionError in api_project_members_remove_by_email: %s", e) | ||
| # Return a controlled error code instead of the raw exception message. | ||
| if str(e) == "not_authenticated": | ||
| error_code = "not_authenticated" | ||
| elif str(e) == "not_found": | ||
| error_code = "not_found" | ||
| else: | ||
| error_code = "forbidden" | ||
| return JSONResponse({"error": error_code}, status_code=code) | ||
| if target_email == str(getattr(p, "owner_email", "") or "").strip().lower(): | ||
| return JSONResponse({"error": "cannot_remove_owner"}, status_code=400) | ||
|
|
| return _project_locked_json_response(e.locked_by_email, e.locked_at) | ||
| except PermissionError as e: | ||
| code = 401 if str(e) == "not_authenticated" else 404 | ||
| return JSONResponse({"error": str(e)}, status_code=code) |
Check warning
Code scanning / CodeQL
Information exposure through an exception Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
In general, the fix is to avoid returning raw exception messages to the client and instead return a generic, non-sensitive error code or message. Optionally, we can log the original exception on the server for debugging purposes, but the client should only see controlled values.
For this specific code at lines 1321–1323 in src/runtimes/ws_server.py, the best fix without changing behavior is to keep using the existing distinction between "not_authenticated" and other PermissionError conditions but avoid exposing the full exception string. A safe pattern is:
- Map
"not_authenticated"to a stable, non-sensitive error identifier like"not_authenticated". - Map all other
PermissionErrorcases to another stable identifier like"forbidden"or"not_found"(since the current code uses 404). - Optionally log the exception via the existing
loggingmodule for diagnostics, but do not send its message back to the client.
Concretely:
- At the
except PermissionError as e:block, introduce a small mapping fromstr(e)to a controlled error code. For instance, ifstr(e) == "not_authenticated", return{"error": "not_authenticated"}with status 401, otherwise return{"error": "not_found"}with status 404. - Do not change any imports;
loggingis already imported at line 7. We can log the exception usinglogging.warningorlogging.errorif desired, but this is optional security-wise. - The changes are confined to the
except PermissionErrorhandler withinapi_git_sync_projectinsrc/runtimes/ws_server.py.
| @@ -1319,8 +1319,13 @@ | ||
| except ProjectLockedError as e: | ||
| return _project_locked_json_response(e.locked_by_email, e.locked_at) | ||
| except PermissionError as e: | ||
| code = 401 if str(e) == "not_authenticated" else 404 | ||
| return JSONResponse({"error": str(e)}, status_code=code) | ||
| # Do not expose raw exception messages to clients; map to controlled error codes. | ||
| is_not_authenticated = str(e) == "not_authenticated" | ||
| code = 401 if is_not_authenticated else 404 | ||
| error_code = "not_authenticated" if is_not_authenticated else "not_found" | ||
| # Optionally log the internal error message for diagnostics. | ||
| logging.warning("Permission error in api_git_sync_project(%s): %s", project_id, e) | ||
| return JSONResponse({"error": error_code}, status_code=code) | ||
|
|
||
| sub = actor["sub"] | ||
| email = actor["email"] |
| { | ||
| "commits": [ | ||
| { | ||
| "sha": c.sha, |
Check warning
Code scanning / CodeQL
Information exposure through an exception Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
In general, to fix this kind of issue you should avoid returning raw exception messages or stack traces in API responses. Instead, log the detailed error on the server (including the stack trace and repr(e)), and return a generic, user-safe error message or a stable error code. This preserves debuggability for developers while preventing attackers from learning about internal implementation details.
For this specific case, we should change the except Exception as e: block around the GitLab commit listing call (lines 1601–1614). Instead of including str(e) in the "detail" field of the JSON response, we should log the exception server-side using the existing logging module (already imported at the top of the file) and return a generic error object without any sensitive details. The surrounding code already distinguishes certain GitLab error states above (e.g., "gitlab_error" cases) and uses "gitlab_error" without details in one branch, so returning a simple "git_history_failed" without "detail" is consistent with existing patterns and does not alter control flow.
Concretely:
- In
src/runtimes/ws_server.py, locate thetry/exceptthat callsGitLabClient.from_env().list_project_commits. - Inside the
except Exception as e::- Add a log statement such as
logging.exception("Failed to list GitLab project commits")to capture the full stack trace on the server. - Change the JSONResponse payload so it no longer includes
str(e), returning only{"error": "git_history_failed"}.
- Add a log statement such as
- No new imports are needed (the
loggingmodule is already imported on line 7).
| @@ -1607,9 +1607,10 @@ | ||
| ref_name=ref_name, | ||
| limit=normalized_limit, | ||
| ) | ||
| except Exception as e: | ||
| except Exception: | ||
| logging.exception("Failed to list GitLab project commits") | ||
| return JSONResponse( | ||
| {"error": "git_history_failed", "detail": str(e)}, | ||
| {"error": "git_history_failed"}, | ||
| status_code=502, | ||
| ) | ||
|
|
| @@ -1709,13 +2111,74 @@ def _ensure_project_access(request: Request, *, project_id: str): | |||
Check warning
Code scanning / CodeQL
Information exposure through an exception Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
In general, to fix this category of issue you should avoid returning raw exception messages (or stack traces) directly to the client. Instead, log the detailed error server-side (if needed) and send a generic, controlled error code or message in the HTTP response. This ensures developers can still debug, but untrusted clients do not see internals.
For this specific endpoint in src/runtimes/ws_server.py, the problematic code is:
1651: except PermissionError as e:
1652: code = 401 if str(e) == "not_authenticated" else 404
1653: return JSONResponse({"error": str(e)}, status_code=code)The best minimal fix without changing behavior from the client’s perspective is:
- Replace returning
str(e)with a controlled, non-sensitive error identifier. - Since the code already uses the string value of the exception to choose between
401and404, we can continue to use that internally but normalize what we send back. - A simple approach is to:
- Map
"not_authenticated"to a fixed"not_authenticated"error code. - Map all other cases to a generic
"not_found"(or similar) error code.
- Map
- Optionally, log the full exception message at server-side using the existing
loggingfacilities (logging.getLogger(__name__)is already likely used earlier in the file). However, since we don’t see the logger here and we must minimize changes, we can just stop exposingstr(e)externally.
Concretely, edit the except PermissionError block in api_git_pull_project so it becomes:
except PermissionError as e:
msg = str(e)
code = 401 if msg == "not_authenticated" else 404
error_code = "not_authenticated" if msg == "not_authenticated" else "not_found"
return JSONResponse({"error": error_code}, status_code=code)This keeps the existing semantics (401 for unauthenticated, 404 otherwise) but no longer leaks arbitrary exception text to clients.
| @@ -1649,8 +1649,10 @@ | ||
| except ProjectLockedError as e: | ||
| return _project_locked_json_response(e.locked_by_email, e.locked_at) | ||
| except PermissionError as e: | ||
| code = 401 if str(e) == "not_authenticated" else 404 | ||
| return JSONResponse({"error": str(e)}, status_code=code) | ||
| msg = str(e) | ||
| code = 401 if msg == "not_authenticated" else 404 | ||
| error_code = "not_authenticated" if msg == "not_authenticated" else "not_found" | ||
| return JSONResponse({"error": error_code}, status_code=code) | ||
|
|
||
| sub = actor["sub"] | ||
| email = actor["email"] |
|
|
||
| body: Any | ||
| try: | ||
| body = await request.json() |
Check warning
Code scanning / CodeQL
Information exposure through an exception Medium
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 1 day ago
In general, the fix is to avoid sending raw exception messages back to clients. Instead, send a generic or controlled error code/message, and if needed, log the detailed exception server-side for debugging. This prevents accidental leakage of stack traces or sensitive internal details through error responses.
For this specific handler in src/runtimes/ws_server.py, we should:
- Keep the logic that chooses
401vs404based on the exception message ("not_authenticated"vs other). - Stop returning
str(e)to the client. - Replace it with a controlled error code string that does not depend on the full exception text, while preserving the 401/404 behavior.
- Optionally, log the original exception using the existing
logginginfrastructure (there is already an importedloggingmodule at the top of the file).
The minimal, behavior-preserving change is:
- In the
except PermissionError as e:block, derive an error code like"not_authenticated"or"not_found"from the same condition used to set the status code and return that instead ofstr(e). That way, clients still get a useful, stable string, but we no longer expose arbitrary exception text.
Concretely:
- In
src/runtimes/ws_server.py, around lines 2542–2544, change:code = 401 if str(e) == "not_authenticated" else 404return JSONResponse({"error": str(e)}, status_code=code)
- To:
- Determine
error_codebased on the same comparison. - Optionally log the error.
return JSONResponse({"error": error_code}, status_code=code)
- Determine
No new imports are strictly necessary; logging is already imported if we want to log the error.
| @@ -2540,8 +2540,9 @@ | ||
| except ProjectLockedError as e: | ||
| return _project_locked_json_response(e.locked_by_email, e.locked_at) | ||
| except PermissionError as e: | ||
| code = 401 if str(e) == "not_authenticated" else 404 | ||
| return JSONResponse({"error": str(e)}, status_code=code) | ||
| error_code = "not_authenticated" if str(e) == "not_authenticated" else "not_found" | ||
| code = 401 if error_code == "not_authenticated" else 404 | ||
| return JSONResponse({"error": error_code}, status_code=code) | ||
|
|
||
| sub = actor["sub"] | ||
| email = actor["email"] |
Summary
Test plan
uv run pytest— all new and existing tests passnpm run build— no TypeScript errors