| | @@ -1133,47 +1133,64 @@ |
| 1133 | 1133 | def fossil_xfer(request, slug): |
| 1134 | 1134 | """Proxy Fossil sync protocol (clone/push/pull) through Django. |
| 1135 | 1135 | |
| 1136 | 1136 | GET — informational page with clone URL. |
| 1137 | 1137 | POST — pipe the request body through ``fossil http`` in CGI mode. |
| 1138 | + |
| 1139 | + Access control: |
| 1140 | + - Public repos: anonymous clone/pull allowed (no --localauth). |
| 1141 | + - Authenticated users with write access: full push/pull (--localauth). |
| 1142 | + - Private/internal repos: require at least read permission. |
| 1138 | 1143 | """ |
| 1139 | | - from projects.access import require_project_read, require_project_write |
| 1144 | + from projects.access import can_read_project, can_write_project |
| 1140 | 1145 | |
| 1141 | 1146 | from .cli import FossilCLI |
| 1142 | 1147 | |
| 1143 | 1148 | project = get_object_or_404(Project, slug=slug, deleted_at__isnull=True) |
| 1144 | 1149 | fossil_repo = get_object_or_404(FossilRepository, project=project, deleted_at__isnull=True) |
| 1145 | 1150 | |
| 1146 | 1151 | if request.method == "GET": |
| 1147 | | - require_project_read(request, project) |
| 1152 | + if not can_read_project(request.user, project): |
| 1153 | + from django.core.exceptions import PermissionDenied |
| 1154 | + |
| 1155 | + raise PermissionDenied |
| 1148 | 1156 | clone_url = request.build_absolute_uri() |
| 1157 | + is_public = project.visibility == "public" |
| 1158 | + auth_note = "" if is_public else "<p>Authentication is required.</p>" |
| 1149 | 1159 | html = ( |
| 1150 | 1160 | f"<html><head><title>{project.name} — Fossil Sync</title></head>" |
| 1151 | 1161 | f"<body>" |
| 1152 | 1162 | f"<h1>{project.name}</h1>" |
| 1153 | 1163 | f"<p>This is the Fossil sync endpoint for <strong>{project.name}</strong>.</p>" |
| 1154 | 1164 | f"<p>Clone with:</p>" |
| 1155 | 1165 | f"<pre>fossil clone {clone_url} {project.slug}.fossil</pre>" |
| 1156 | | - f"<p>Authentication is required.</p>" |
| 1166 | + f"{auth_note}" |
| 1157 | 1167 | f"</body></html>" |
| 1158 | 1168 | ) |
| 1159 | 1169 | return HttpResponse(html) |
| 1160 | 1170 | |
| 1161 | 1171 | if request.method == "POST": |
| 1162 | | - # All POST sync operations (push/pull/clone) require write access for |
| 1163 | | - # now. We can loosen pull to read-only once we can distinguish the |
| 1164 | | - # operation from the request payload. |
| 1165 | | - require_project_write(request, project) |
| 1166 | | - |
| 1167 | 1172 | if not fossil_repo.exists_on_disk: |
| 1168 | 1173 | raise Http404("Repository file not found on disk.") |
| 1169 | 1174 | |
| 1175 | + has_write = can_write_project(request.user, project) |
| 1176 | + has_read = can_read_project(request.user, project) |
| 1177 | + |
| 1178 | + if not has_read: |
| 1179 | + from django.core.exceptions import PermissionDenied |
| 1180 | + |
| 1181 | + raise PermissionDenied |
| 1182 | + |
| 1183 | + # With --localauth, fossil grants full push access (for authenticated |
| 1184 | + # writers). Without it, fossil only allows pull/clone (for anonymous |
| 1185 | + # or read-only users on public repos). |
| 1170 | 1186 | cli = FossilCLI() |
| 1171 | 1187 | body, content_type = cli.http_proxy( |
| 1172 | 1188 | fossil_repo.full_path, |
| 1173 | 1189 | request.body, |
| 1174 | 1190 | request.content_type, |
| 1191 | + localauth=has_write, |
| 1175 | 1192 | ) |
| 1176 | 1193 | return HttpResponse(body, content_type=content_type) |
| 1177 | 1194 | |
| 1178 | 1195 | return HttpResponse(status=405) |
| 1179 | 1196 | |
| 1180 | 1197 | |