1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
"""
FastAPI dependencies for shared request validation (exported repositories, etc.).
"""
from __future__ import annotations
import ipaddress
from typing import Annotated, Any
from fastapi import Depends, Form, HTTPException, Query, Request
from pygitweb.auth import (
access_token_from_request,
ensure_read_project,
ensure_write_project,
)
from pygitweb.config import settings
from pygitweb.projects import git_get_projects_list, project_in_list
from pygitweb.validation import is_valid_project
def _visible_projects_list() -> list[dict[str, Any]]:
return git_get_projects_list(
filter_path="",
paranoid=settings.STRICT_EXPORT,
export_ok=settings.EXPORT_OK,
)
def project_visible_in_list(project: str) -> bool:
"""True if ``project`` appears in the same listing as the project picker."""
return project_in_list(project, _visible_projects_list)
def require_valid_project(project: str) -> str:
"""Reject empty or unknown project paths (export_ok, strict list, etc.)."""
if not project or project == "":
raise HTTPException(status_code=400, detail="Project needed")
if not is_valid_project(
project,
settings.PROJECTROOT,
settings.EXPORT_OK,
settings.STRICT_EXPORT,
project_visible_in_list,
):
raise HTTPException(status_code=404, detail="No such project")
return project
def require_valid_project_name(name: str) -> str:
"""Same as ``require_valid_project`` for a path parameter named ``name``."""
return require_valid_project(name)
def require_valid_project_from_query(
project: Annotated[str, Query(..., description="Project path")],
) -> str:
return require_valid_project(project)
def require_valid_project_form(project: Annotated[str, Form()]) -> str:
return require_valid_project(project)
def require_readable_project(
project: str,
token: Annotated[str | None, Depends(access_token_from_request)],
) -> str:
validated = require_valid_project(project)
ensure_read_project(validated, token)
return validated
def require_readable_project_from_query(
project: Annotated[str, Query(..., description="Project path")],
token: Annotated[str | None, Depends(access_token_from_request)],
) -> str:
return require_readable_project(project, token)
def require_readable_project_form(
project: Annotated[str, Form()],
token: Annotated[str | None, Depends(access_token_from_request)],
) -> str:
return require_readable_project(project, token)
def require_readable_project_name(
name: str,
token: Annotated[str | None, Depends(access_token_from_request)],
) -> str:
return require_readable_project(name, token)
def require_writable_project_name(
name: str,
token: Annotated[str | None, Depends(access_token_from_request)],
) -> str:
validated = require_valid_project(name)
ensure_write_project(validated, token)
return validated
def is_loopback_host(host: str | None) -> bool:
if not host:
return False
if host == "localhost":
return True
try:
return ipaddress.ip_address(host).is_loopback
except ValueError:
return False
def require_loopback_client(request: Request) -> None:
"""Reject non-local callers (hooks should POST from the same host)."""
client = request.client
if client is not None and is_loopback_host(client.host):
return
raise HTTPException(status_code=403, detail="Forbidden")
def require_notify_project(
project: Annotated[str | None, Query(alias="project", description="Repository path")] = None,
) -> str:
"""Internal notify: project must appear in the visible list (hook / long-poll contract)."""
if not project or not project.strip():
raise HTTPException(status_code=400, detail="project query parameter required")
p = project.strip()
if not project_visible_in_list(p):
raise HTTPException(status_code=404, detail="No such project")
return p
def board_create_resolved_project(
token: Annotated[str | None, Depends(access_token_from_request)],
p: Annotated[str | None, Query(alias="p")] = None,
project: Annotated[str | None, Query(alias="project")] = None,
) -> str:
"""Resolve ``?p=`` / ``?project=`` then validate as a normal project path."""
chosen = (project or p or "").strip()
if not chosen:
raise HTTPException(status_code=400, detail="Project needed (use ?project= or ?p=)")
return require_readable_project(chosen, token)
def filter_projects_by_read_access(
projects: list[dict[str, Any]],
token: str | None,
) -> list[dict[str, Any]]:
from pygitweb.auth import can_read_project
return [p for p in projects if can_read_project(p.get("path", ""), token)]
ValidatedPathProject = Annotated[str, Depends(require_valid_project)]
ValidatedReadableProject = Annotated[str, Depends(require_readable_project)]
ValidatedQueryProject = Annotated[str, Depends(require_valid_project_from_query)]
ValidatedReadableQueryProject = Annotated[str, Depends(require_readable_project_from_query)]
ValidatedSettingsProject = Annotated[str, Depends(require_valid_project_name)]
ValidatedReadableSettingsProject = Annotated[str, Depends(require_readable_project_name)]
ValidatedWritableSettingsProject = Annotated[str, Depends(require_writable_project_name)]
ValidatedBoardCreateProject = Annotated[str, Depends(board_create_resolved_project)]
ValidatedNotifyProject = Annotated[str, Depends(require_notify_project)]
ValidatedFormProject = Annotated[str, Depends(require_valid_project_form)]
ValidatedReadableFormProject = Annotated[str, Depends(require_readable_project_form)]