You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/01/22 14:59:57 UTC
(tooling-trusted-releases) 03/03: #476 - allow rsync to specify a tag as part of the URL
This is an automated email from the ASF dual-hosted git repository.
arm pushed a commit to branch atr_tagging
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit 35b3d99732c3536511b8d8c74377ffb347e93cbb
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Thu Jan 22 14:59:39 2026 +0000
#476 - allow rsync to specify a tag as part of the URL
---
atr/ssh.py | 109 +++++++++++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 89 insertions(+), 20 deletions(-)
diff --git a/atr/ssh.py b/atr/ssh.py
index 2b153b3..93e37d9 100644
--- a/atr/ssh.py
+++ b/atr/ssh.py
@@ -20,6 +20,7 @@
import asyncio
import asyncio.subprocess
import datetime
+import glob
import os
import stat
import string
@@ -233,7 +234,9 @@ async def _step_02_handle_safely(process: asyncssh.SSHServerProcess, server: SSH
#######################################
### Calls _step_04_command_validate ###
#######################################
- project_name, version_name, release_obj = await _step_04_command_validate(process, argv, is_read_request, server)
+ project_name, version_name, file_patterns, release_obj = await _step_04_command_validate(
+ process, argv, is_read_request, server
+ )
# The release object is only present for read requests
release_name = sql.release_name(project_name, version_name)
@@ -242,7 +245,7 @@ async def _step_02_handle_safely(process: asyncssh.SSHServerProcess, server: SSH
####################################################
### Calls _step_07a_process_validated_rsync_read ###
####################################################
- await _step_07a_process_validated_rsync_read(process, argv, release_obj)
+ await _step_07a_process_validated_rsync_read(process, argv, release_obj, file_patterns)
else:
_output_stderr(process, f"Received write command: {process.command}")
log.info(f"Processing WRITE request for {release_name}")
@@ -297,12 +300,15 @@ def _step_03_command_simple_validate(argv: list[str]) -> bool:
async def _step_04_command_validate(
process: asyncssh.SSHServerProcess, argv: list[str], is_read_request: bool, server: SSHServer
-) -> tuple[str, str, sql.Release | None]:
+) -> tuple[str, str, list[str] | None, sql.Release | None]:
"""Validate the path and user permissions for read or write."""
############################################
- ### Calls _step_05_command_path_validate ###
+ ### Calls _step_05a/b_command_path_validate ###
############################################
- path_project, path_version = _step_05_command_path_validate(argv[-1])
+ if is_read_request:
+ path_project, path_version, tag = _step_05a_command_path_validate_read(argv[-1])
+ else:
+ path_project, path_version, tag = _step_05b_command_path_validate_write(argv[-1])
ssh_uid = server._get_asf_uid(process)
@@ -312,26 +318,71 @@ async def _step_04_command_validate(
# Projects are public, so existence information is public
raise RsyncArgsError(f"Project '{path_project}' does not exist")
- release = await data.release(project_name=project.name, version=path_version).get()
+ release = await data.release(project_name=project.name, version=path_version, _release_policy=True).get()
if is_read_request:
#################################################
### Calls _step_06a_validate_read_permissions ###
#################################################
- validated_release = await _step_06a_validate_read_permissions(
- ssh_uid, project, release, path_project, path_version
+ validated_release, file_patterns = await _step_06a_validate_read_permissions(
+ ssh_uid, project, release, path_project, path_version, tag
)
- return path_project, path_version, validated_release
+ return path_project, path_version, file_patterns, validated_release
##################################################
### Calls _step_06b_validate_write_permissions ###
##################################################
await _step_06b_validate_write_permissions(ssh_uid, project, release)
- # Return None for the release object for write requests
- return path_project, path_version, None
+ # Return None for the tag and release objects for write requests
+ return path_project, path_version, None, None
-def _step_05_command_path_validate(path: str) -> tuple[str, str]:
+def _step_05a_command_path_validate_read(path: str) -> tuple[str, str, str | None]:
+ """Validate the path argument for rsync commands."""
+ # READ: rsync --server --sender -vlogDtpre.iLsfxCIvu . /proj/v1/
+ # Validating path: /proj/v1/
+ # WRITE: rsync --server -vlogDtpre.iLsfxCIvu . /proj/v1/
+ # Validating path: /proj/v1/
+
+ if not path.startswith("/"):
+ raise RsyncArgsError("The path argument should be an absolute path")
+
+ if not path.endswith("/"):
+ # Technically we could ignore this, because we rewrite the path anyway for writes
+ # But we should enforce good rsync usage practices
+ raise RsyncArgsError("The path argument should be a directory path, ending with a /")
+
+ if "//" in path:
+ raise RsyncArgsError("The path argument should not contain //")
+
+ if path.count("/") < 3 or path.count("/") > 4:
+ raise RsyncArgsError("The path argument should be a /PROJECT/VERSION/(tag)/ directory path")
+
+ path_project, path_version, *rest = path.strip("/").split("/", 2)
+ tag = rest[0] if rest else None
+ alphanum = set(string.ascii_letters + string.digits + "-")
+ if not all(c in alphanum for c in path_project):
+ raise RsyncArgsError("The project name should contain only alphanumeric characters or hyphens")
+
+ if tag and (not all(c in alphanum for c in path_project)):
+ raise RsyncArgsError("The tag should contain only alphanumeric characters or hyphens")
+
+ # From a survey of version numbers we find that only . and - are used
+ # We also allow + which is in common use
+ version_punctuation = set(".-+")
+ if path_version[0] not in alphanum:
+ # Must certainly not allow the directory to be called "." or ".."
+ # And we also want to avoid patterns like ".htaccess"
+ raise RsyncArgsError("The version should start with an alphanumeric character")
+ if path_version[-1] not in alphanum:
+ raise RsyncArgsError("The version should end with an alphanumeric character")
+ if not all(c in (alphanum | version_punctuation) for c in path_version):
+ raise RsyncArgsError("The version should contain only alphanumeric characters, dots, dashes, or pluses")
+
+ return path_project, path_version, tag
+
+
+def _step_05b_command_path_validate_write(path: str) -> tuple[str, str, str | None]:
"""Validate the path argument for rsync commands."""
# READ: rsync --server --sender -vlogDtpre.iLsfxCIvu . /proj/v1/
# Validating path: /proj/v1/
@@ -352,11 +403,15 @@ def _step_05_command_path_validate(path: str) -> tuple[str, str]:
if path.count("/") != 3:
raise RsyncArgsError("The path argument should be a /PROJECT/VERSION/ directory path")
- path_project, path_version = path.strip("/").split("/", 1)
+ path_project, path_version, *rest = path.strip("/").split("/", 2)
+ tag = rest[0] if rest else None
alphanum = set(string.ascii_letters + string.digits + "-")
if not all(c in alphanum for c in path_project):
raise RsyncArgsError("The project name should contain only alphanumeric characters or hyphens")
+ if tag and (not all(c in alphanum for c in path_project)):
+ raise RsyncArgsError("The tag should contain only alphanumeric characters or hyphens")
+
# From a survey of version numbers we find that only . and - are used
# We also allow + which is in common use
version_punctuation = set(".-+")
@@ -369,7 +424,7 @@ def _step_05_command_path_validate(path: str) -> tuple[str, str]:
if not all(c in (alphanum | version_punctuation) for c in path_version):
raise RsyncArgsError("The version should contain only alphanumeric characters, dots, dashes, or pluses")
- return path_project, path_version
+ return path_project, path_version, tag
async def _step_06a_validate_read_permissions(
@@ -378,7 +433,8 @@ async def _step_06a_validate_read_permissions(
release: sql.Release | None,
path_project: str,
path_version: str,
-) -> sql.Release | None:
+ tag: str | None,
+) -> tuple[sql.Release | None, list[str] | None]:
"""Validate permissions for a read request."""
if release is None:
raise RsyncArgsError(f"Release '{path_project}-{path_version}' does not exist")
@@ -396,7 +452,15 @@ async def _step_06a_validate_read_permissions(
raise RsyncArgsError(
f"You must be a committer or committee member for project '{project.name}' to read this release"
)
- return release
+
+ if tag:
+ if not release.release_policy or (not release.release_policy.atr_file_tagging_spec):
+ raise RsyncArgsError(f"Release '{release.name}' does not support tags")
+ tags = release.release_policy.atr_file_tagging_spec.keys()
+ if tag not in tags:
+ raise RsyncArgsError(f"Tag '{tag}' is not allowed for release '{release.name}'")
+ return release, release.release_policy.atr_file_tagging_spec[tag]
+ return release, None
async def _step_06b_validate_write_permissions(
@@ -426,6 +490,7 @@ async def _step_07a_process_validated_rsync_read(
process: asyncssh.SSHServerProcess,
argv: list[str],
release: sql.Release,
+ file_patterns: list[str] | None,
) -> None:
"""Handle a validated rsync read request."""
exit_status = 1
@@ -441,10 +506,14 @@ async def _step_07a_process_validated_rsync_read(
if not await aiofiles.os.path.isdir(source_dir):
raise RsyncArgsError(f"Source directory '{source_dir}' not found for release {release.name}")
- # Update the rsync command path to the determined source directory
- argv[-1] = str(source_dir)
- if not argv[-1].endswith("/"):
- argv[-1] += "/"
+ if file_patterns is None:
+ # Update the rsync command path to the determined source directory
+ argv[-1] = str(source_dir)
+ if not argv[-1].endswith("/"):
+ argv[-1] += "/"
+ else:
+ files = [f for pattern in file_patterns for f in glob.glob(f"{source_dir}/{pattern}")]
+ argv[-1:] = files
###################################################
### Calls _step_08_execute_rsync_sender_command ###
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org