44
55from fastapi import Depends , HTTPException , Security , status
66from fastapi .security import HTTPAuthorizationCredentials , HTTPBearer
7+ from sqlalchemy import select
78
89from testgen .common .auth import authorize_token , decode_jwt_token
910from testgen .common .models import Session , _current_session_wrapper , get_current_session
11+ from testgen .common .models .job_execution import PUBLIC_JOB_KEYS , JobExecution
1012from testgen .common .models .project_membership import ProjectMembership
13+ from testgen .common .models .table_group import TableGroup
14+ from testgen .common .models .test_suite import TestSuite
1115from testgen .common .models .user import User
1216from testgen .utils .plugins import PluginHook
1317
@@ -73,14 +77,25 @@ def has_project_permission(user: User, project_code: str, permission: str) -> bo
7377
7478# --- Resolver dependency factories ---
7579# Each factory takes a permission string and returns Depends(). The entity ID
76- # comes from a URL path parameter (FastAPI resolves it natively).
77- # Entity not found and insufficient permission both raise the same 404
78- # with a stable code/message — no variation that could leak the cause.
80+ # comes from a URL path parameter (FastAPI resolves it natively, including
81+ # UUID validation that yields a 422 for malformed inputs).
7982
8083_require_user = Depends (get_authorized_user )
8184_not_found = api_error (404 , "not_found" , "Not found" )
8285
8386
87+ def _check_access (entity , user : User , permission : str ):
88+ """Return ``entity`` if the user has ``permission`` on its project, else raise 404.
89+
90+ Entity-not-found and insufficient-permission both surface as the same 404
91+ with a stable code/message — no variation that could leak the cause to an
92+ unauthorized caller.
93+ """
94+ if entity and has_project_permission (user , entity .project_code , permission ):
95+ return entity
96+ raise _not_found
97+
98+
8499def resolve_project_code (permission : str ):
85100 """Verify the user has ``permission`` on the project identified by ``project_code`` path param."""
86101 def dependency (project_code : str , user : User = _require_user ) -> str :
@@ -92,45 +107,31 @@ def dependency(project_code: str, user: User = _require_user) -> str:
92107
93108def resolve_table_group (permission : str ):
94109 """Resolve a TableGroup by ``table_group_id`` path param and verify project permission."""
95- from testgen .common .models .table_group import TableGroup
96-
97110 def dependency (table_group_id : UUID , user : User = _require_user ) -> TableGroup :
98- if (table_group := TableGroup .get (table_group_id )) and has_project_permission (user , table_group .project_code , permission ):
99- return table_group
100- raise _not_found
111+ return _check_access (TableGroup .get (table_group_id ), user , permission )
101112 return Depends (dependency )
102113
103114
104115def resolve_test_suite (permission : str ):
105116 """Resolve a non-monitor TestSuite by ``test_suite_id`` path param and verify project permission."""
106- from testgen .common .models .test_suite import TestSuite
107-
108117 def dependency (test_suite_id : UUID , user : User = _require_user ) -> TestSuite :
109- if (test_suite := TestSuite .get_regular (test_suite_id )) and has_project_permission (user , test_suite .project_code , permission ):
110- return test_suite
111- raise _not_found
118+ return _check_access (TestSuite .get_regular (test_suite_id ), user , permission )
112119 return Depends (dependency )
113120
114121
115122def resolve_job (permission : str , * extra_filters ):
116123 """Resolve a JobExecution by ``job_id`` path param and verify project permission.
117124
118- Internally-submitted jobs (source='system') are never exposed via the API.
119- Extra ORM clauses are appended to the WHERE clause, e.g. to restrict by job_key.
120- Mismatches surface as the same 404 — no information leakage.
125+ Only jobs whose ``job_key`` is in ``PUBLIC_JOB_KEYS`` are exposed via the API.
126+ Internal kinds (score rollups, recalculations, monitor runs) are filtered out
127+ by construction. Extra ORM clauses are appended to the WHERE clause to further
128+ restrict by job_key when a caller wants a single kind.
121129 """
122- from sqlalchemy import select
123-
124- from testgen .common .models .job_execution import JobExecution
125-
126130 def dependency (job_id : UUID , user : User = _require_user ) -> JobExecution :
127131 query = select (JobExecution ).where (
128132 JobExecution .id == job_id ,
129- JobExecution .source != "system" ,
133+ JobExecution .job_key . in_ ( PUBLIC_JOB_KEYS ) ,
130134 * extra_filters ,
131135 )
132- job = get_current_session ().scalars (query ).first ()
133- if job and has_project_permission (user , job .project_code , permission ):
134- return job
135- raise _not_found
136+ return _check_access (get_current_session ().scalars (query ).first (), user , permission )
136137 return Depends (dependency )
0 commit comments