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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 39dd1b95a0 [python] Improve OpenAPI nonce generation and parameter 
validation to… (#7274)
39dd1b95a0 is described below

commit 39dd1b95a03e5b23d2a7a955357eb0aff0f13e61
Author: Dapeng Sun(孙大鹏) <[email protected]>
AuthorDate: Sun May 24 10:07:22 2026 +0800

    [python] Improve OpenAPI nonce generation and parameter validation to… 
(#7274)
---
 paimon-python/pypaimon/api/auth/dlf_signer.py      | 52 ++++++++----
 .../pypaimon/tests/rest/dlf_signer_test.py         | 99 ++++++++++++++++++++++
 2 files changed, 133 insertions(+), 18 deletions(-)

diff --git a/paimon-python/pypaimon/api/auth/dlf_signer.py 
b/paimon-python/pypaimon/api/auth/dlf_signer.py
index fa1b2c636b..8f6f3871bd 100644
--- a/paimon-python/pypaimon/api/auth/dlf_signer.py
+++ b/paimon-python/pypaimon/api/auth/dlf_signer.py
@@ -18,10 +18,12 @@
 import base64
 import hashlib
 import hmac
+import threading
+import time
 import uuid
 from abc import ABC, abstractmethod
 from collections import OrderedDict
-from datetime import datetime
+from datetime import datetime, timezone
 from typing import Dict, Optional
 from urllib.parse import unquote
 
@@ -320,15 +322,21 @@ class DLFOpenApiSigner(DLFRequestSigner):
             security_token: Optional[str],
             host: str
     ) -> Dict[str, str]:
+        if now is None:
+            raise ValueError("Parameter 'now' cannot be None")
+        if host is None:
+            raise ValueError("Parameter 'host' cannot be None")
+
         headers = {}
 
-        # Date header in RFC 1123 format
-        headers[self.DATE_HEADER] = now.strftime(self.DATE_FORMAT)
+        if now.tzinfo is None:
+            gmt_time = now.replace(tzinfo=timezone.utc)
+        else:
+            gmt_time = now.astimezone(timezone.utc)
+        headers[self.DATE_HEADER] = gmt_time.strftime(self.DATE_FORMAT)
 
-        # Accept header
         headers[self.ACCEPT_HEADER] = self.ACCEPT_VALUE
 
-        # Content-MD5 (if body exists)
         if body is not None and body != "":
             try:
                 headers[self.CONTENT_MD5_HEADER] = self._md5_base64(body)
@@ -336,16 +344,15 @@ class DLFOpenApiSigner(DLFRequestSigner):
             except Exception as e:
                 raise RuntimeError(f"Failed to calculate Content-MD5: {e}")
 
-        # Host header
         headers[self.HOST_HEADER] = host
 
-        # x-acs-* headers
         headers[self.X_ACS_SIGNATURE_METHOD] = self.SIGNATURE_METHOD_VALUE
-        headers[self.X_ACS_SIGNATURE_NONCE] = str(uuid.uuid4())
+
+        nonce = self._generate_unique_nonce()
+        headers[self.X_ACS_SIGNATURE_NONCE] = nonce
         headers[self.X_ACS_SIGNATURE_VERSION] = self.SIGNATURE_VERSION_VALUE
         headers[self.X_ACS_VERSION] = self.API_VERSION
 
-        # Security token (if present)
         if security_token is not None:
             headers[self.X_ACS_SECURITY_TOKEN] = security_token
 
@@ -358,22 +365,22 @@ class DLFOpenApiSigner(DLFRequestSigner):
             host: str,
             sign_headers: Dict[str, str]
     ) -> str:
+        if rest_auth_parameter is None:
+            raise ValueError("Parameter 'rest_auth_parameter' cannot be None")
+        if token is None:
+            raise ValueError("Parameter 'token' cannot be None")
+        if host is None:
+            raise ValueError("Parameter 'host' cannot be None")
+        if sign_headers is None:
+            raise ValueError("Parameter 'sign_headers' cannot be None")
+
         try:
-            # Step 1: Build CanonicalizedHeaders (x-acs-* headers, sorted, 
lowercase)
             canonicalized_headers = 
self._build_canonicalized_headers(sign_headers)
-
-            # Step 2: Build CanonicalizedResource (path + sorted query string)
             canonicalized_resource = 
self._build_canonicalized_resource(rest_auth_parameter)
-
-            # Step 3: Build StringToSign
             string_to_sign = self._build_string_to_sign(
                 rest_auth_parameter, sign_headers, canonicalized_headers, 
canonicalized_resource
             )
-
-            # Step 4: Calculate signature
             signature = self._calculate_signature(string_to_sign, 
token.access_key_secret)
-
-            # Step 5: Build Authorization header
             return f"acs {token.access_key_id}:{signature}"
 
         except Exception as e:
@@ -462,6 +469,15 @@ class DLFOpenApiSigner(DLFRequestSigner):
         except Exception as e:
             raise RuntimeError(f"Failed to calculate signature: {e}")
 
+    def _generate_unique_nonce(self) -> str:
+        """Generate unique nonce with UUID, timestamp, and thread ID."""
+        unique_nonce = []
+        uuid_val = str(uuid.uuid4())
+        unique_nonce.append(uuid_val)
+        unique_nonce.append(str(int(time.time() * 1000)))
+        unique_nonce.append(str(threading.current_thread().ident))
+        return "".join(unique_nonce)
+
     @staticmethod
     def _md5_base64(data: str) -> str:
         md5_hash = hashlib.md5(data.encode("utf-8")).digest()
diff --git a/paimon-python/pypaimon/tests/rest/dlf_signer_test.py 
b/paimon-python/pypaimon/tests/rest/dlf_signer_test.py
index 5ddd7d8ca1..bbda410aec 100644
--- a/paimon-python/pypaimon/tests/rest/dlf_signer_test.py
+++ b/paimon-python/pypaimon/tests/rest/dlf_signer_test.py
@@ -16,6 +16,8 @@
 # under the License.
 
 import unittest
+import re
+import threading
 from datetime import datetime, timezone
 
 from pypaimon.api.auth import (
@@ -158,6 +160,103 @@ class DLFSignerTest(unittest.TestCase):
         self.assertEqual("default", parse(""))
         self.assertEqual("default", parse(None))
 
+    def test_openapi_sign_headers_with_enhanced_nonce(self):
+        """Test enhanced nonce generation."""
+        signer = DLFOpenApiSigner()
+        body = '{"CategoryName":"test","CategoryType":"UNSTRUCTURED"}'
+        now = datetime(2025, 4, 16, 3, 44, 46, tzinfo=timezone.utc)
+        host = "dlfnext.cn-beijing.aliyuncs.com"
+
+        headers = signer.sign_headers(body, now, None, host)
+
+        self.assertIsNotNone(headers.get("Date"))
+        self.assertEqual("application/json", headers.get("Accept"))
+        self.assertIsNotNone(headers.get("Content-MD5"))
+        self.assertEqual("application/json", headers.get("Content-Type"))
+        self.assertEqual(host, headers.get("Host"))
+        self.assertEqual("HMAC-SHA1", headers.get("x-acs-signature-method"))
+
+        nonce_value = headers.get("x-acs-signature-nonce")
+        self.assertIsNotNone(nonce_value)
+
+        uuid_pattern = 
re.compile(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
+        uuid_match = uuid_pattern.search(nonce_value)
+        self.assertIsNotNone(uuid_match, f"No UUID pattern found in nonce: 
{nonce_value}")
+
+        digit_pattern = re.compile(r'\d+')
+        digit_matches = digit_pattern.findall(nonce_value)
+        self.assertGreater(len(digit_matches), 0, f"No numeric parts found in 
nonce: {nonce_value}")
+
+        timestamp_found = any(len(part) >= 10 for part in digit_matches)
+        self.assertTrue(timestamp_found, f"No timestamp-like part found in 
nonce: {nonce_value}")
+
+        self.assertEqual("1.0", headers.get("x-acs-signature-version"))
+        self.assertEqual("2026-01-18", headers.get("x-acs-version"))
+
+    def test_concurrent_nonce_generation(self):
+        """Test nonce generation thread safety."""
+        signer = DLFOpenApiSigner()
+        body = '{"test":"data"}'
+        now = datetime.now(timezone.utc)
+        host = "test-host"
+        thread_count = 10
+        iterations_per_thread = 50
+
+        nonces = set()
+
+        def worker():
+            for _ in range(iterations_per_thread):
+                headers = signer.sign_headers(body, now, None, host)
+                nonce = headers.get("x-acs-signature-nonce")
+                nonces.add(nonce)
+
+        threads = []
+        for _ in range(thread_count):
+            thread = threading.Thread(target=worker)
+            threads.append(thread)
+            thread.start()
+
+        for thread in threads:
+            thread.join()
+
+        expected_total = thread_count * iterations_per_thread
+        self.assertEqual(expected_total, len(nonces),
+                         f"Expected {expected_total} unique nonces, but got 
{len(nonces)}. "
+                         f"Possible duplicate nonces generated.")
+
+    def test_parameter_validation(self):
+        """Test parameter validation."""
+        signer = DLFOpenApiSigner()
+        
+        with self.assertRaises(ValueError) as context:
+            signer.sign_headers("body", None, "token", "host")
+        self.assertIn("'now' cannot be None", str(context.exception))
+        
+        now = datetime.now(timezone.utc)
+        with self.assertRaises(ValueError) as context:
+            signer.sign_headers("body", now, "token", None)
+        self.assertIn("'host' cannot be None", str(context.exception))
+        
+        token = DLFToken("ak", "sk", "token", None)
+        rest_param = RESTAuthParameter("GET", "/", "", {})
+        headers = signer.sign_headers("", now, "", "host")
+        
+        with self.assertRaises(ValueError) as context:
+            signer.authorization(None, token, "host", headers)
+        self.assertIn("'rest_auth_parameter' cannot be None", 
str(context.exception))
+        
+        with self.assertRaises(ValueError) as context:
+            signer.authorization(rest_param, None, "host", headers)
+        self.assertIn("'token' cannot be None", str(context.exception))
+        
+        with self.assertRaises(ValueError) as context:
+            signer.authorization(rest_param, token, None, headers)
+        self.assertIn("'host' cannot be None", str(context.exception))
+        
+        with self.assertRaises(ValueError) as context:
+            signer.authorization(rest_param, token, "host", None)
+        self.assertIn("'sign_headers' cannot be None", str(context.exception))
+
 
 if __name__ == '__main__':
     unittest.main()

Reply via email to