This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new 005cf6e  Only allow SVN imports from known locations
005cf6e is described below

commit 005cf6ea41defb57fa902b71c947e3c2bd81d306
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Dec 16 16:22:35 2025 +0000

    Only allow SVN imports from known locations
---
 atr/form.py          | 26 ++++++++++++++++++++++++++
 atr/get/upload.py    |  2 +-
 atr/post/upload.py   | 26 +++++++++++++++++++++++++-
 atr/shared/upload.py | 18 ++++++++++++++----
 4 files changed, 66 insertions(+), 6 deletions(-)

diff --git a/atr/form.py b/atr/form.py
index ac155dd..0f3dba0 100644
--- a/atr/form.py
+++ b/atr/form.py
@@ -459,6 +459,26 @@ def to_str_list(v: Any) -> list[str]:
     raise ValueError(f"Expected a string or list of strings, got 
{type(v).__name__}")
 
 
+def to_url_path(v: Any) -> str | None:
+    if not v:
+        return None
+
+    url_path = str(v)
+
+    if url_path.startswith("/"):
+        raise ValueError("Absolute paths are not allowed")
+
+    segments = url_path.split("/")
+
+    if "." in segments:
+        raise ValueError("Self directory references (.) are not allowed")
+
+    if ".." in segments:
+        raise ValueError("Parent directory references (..) are not allowed")
+
+    return url_path
+
+
 # Validator types come before other functions
 # We must not use the "type" keyword here, otherwise Pydantic complains
 
@@ -521,6 +541,12 @@ StrList = Annotated[
     pydantic.Field(default_factory=list),
 ]
 
+URLPath = Annotated[
+    str | None,
+    functional_validators.BeforeValidator(to_url_path),
+    pydantic.Field(default=None),
+]
+
 
 class Set[EnumType: enum.Enum]:
     def __iter__(self) -> Iterator[EnumType]:
diff --git a/atr/get/upload.py b/atr/get/upload.py
index 8bfa66f..b60c6ef 100644
--- a/atr/get/upload.py
+++ b/atr/get/upload.py
@@ -71,7 +71,7 @@ async def selected(session: web.Committer, project_name: str, 
version_name: str)
     )
 
     block.h2(id="svn-upload")["SVN upload"]
-    block.p["Import files from a world readable Subversion repository URL into 
this draft."]
+    block.p["Import files from this project's ASF Subversion repository into 
this draft."]
     block.p[
         "The import will be processed in the background using the ",
         htm.code["svn export"],
diff --git a/atr/post/upload.py b/atr/post/upload.py
index f8404e3..f0dc590 100644
--- a/atr/post/upload.py
+++ b/atr/post/upload.py
@@ -16,15 +16,20 @@
 # under the License.
 
 
+from typing import Final
+
 import quart
 
 import atr.blueprints.post as post
+import atr.db as db
 import atr.get as get
 import atr.log as log
 import atr.shared as shared
 import atr.storage as storage
 import atr.web as web
 
+_SVN_BASE_URL: Final[str] = "https://dist.apache.org/repos/dist";
+
 
 @post.committer("/upload/<project_name>/<version_name>")
 @post.form(shared.upload.UploadForm)
@@ -69,17 +74,36 @@ async def _add_files(
         )
 
 
+def _construct_svn_url(project_name: str, area: shared.upload.SvnArea, path: 
str, *, is_podling: bool) -> str:
+    if is_podling:
+        return f"{_SVN_BASE_URL}/{area.value}/incubator/{project_name}/{path}"
+    return f"{_SVN_BASE_URL}/{area.value}/{project_name}/{path}"
+
+
 async def _svn_import(
     session: web.Committer, svn_form: shared.upload.SvnImportForm, 
project_name: str, version_name: str
 ) -> web.WerkzeugResponse:
     try:
         target_subdirectory = str(svn_form.target_subdirectory) if 
svn_form.target_subdirectory else None
+        svn_area = svn_form.svn_area
+        svn_path = svn_form.svn_path or ""
+
+        async with db.session() as data:
+            release = await session.release(project_name, version_name, 
data=data)
+            is_podling = (release.project.committee is not None) and 
release.project.committee.is_podling
+
+        svn_url = _construct_svn_url(
+            project_name,
+            svn_area,  # pyright: ignore[reportArgumentType]
+            svn_path,
+            is_podling=is_podling,
+        )
         async with storage.write(session) as write:
             wacp = await write.as_project_committee_participant(project_name)
             await wacp.release.import_from_svn(
                 project_name,
                 version_name,
-                str(svn_form.svn_url),
+                svn_url,
                 svn_form.revision,
                 target_subdirectory,
             )
diff --git a/atr/shared/upload.py b/atr/shared/upload.py
index cf4abb2..7fd532d 100644
--- a/atr/shared/upload.py
+++ b/atr/shared/upload.py
@@ -15,6 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import enum
 from typing import Annotated, Literal
 
 import pydantic
@@ -25,6 +26,11 @@ type ADD_FILES = Literal["add_files"]
 type SVN_IMPORT = Literal["svn_import"]
 
 
+class SvnArea(enum.Enum):
+    DEV = "dev"
+    RELEASE = "release"
+
+
 class AddFilesForm(form.Form):
     variant: ADD_FILES = form.value(ADD_FILES)
     file_data: form.FileList = form.label("Files", "Select the files to 
upload.")
@@ -50,10 +56,14 @@ class AddFilesForm(form.Form):
 
 class SvnImportForm(form.Form):
     variant: SVN_IMPORT = form.value(SVN_IMPORT)
-    svn_url: form.URL = form.label(
-        "SVN URL",
-        "The HTTP or HTTPS URL to the public SVN directory.",
-        widget=form.Widget.URL,
+    svn_area: form.Enum[SvnArea] = form.label(
+        "SVN area",
+        "Select whether to import from dev or release.",
+        widget=form.Widget.RADIO,
+    )
+    svn_path: form.URLPath = form.label(
+        "SVN path",
+        "Path within the project's SVN directory, e.g. 'java-library/4_0_4' or 
'3.1.5rc1'.",
     )
     revision: str = form.label(
         "Revision",


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to