Skip to content

Commit bb1d9a5

Browse files
committed
feat: add create_storage_content_signature
1 parent 85a6d4c commit bb1d9a5

File tree

2 files changed

+60
-0
lines changed

2 files changed

+60
-0
lines changed

src/apify_shared/utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from __future__ import annotations
22

3+
import base64
34
import contextlib
45
import hashlib
56
import hmac
67
import io
78
import json
89
import re
910
import string
11+
import time
1012
from datetime import datetime, timezone
1113
from enum import Enum
1214
from typing import Any, TypeVar, cast
@@ -153,3 +155,24 @@ def create_hmac_signature(secret_key: str, message: str) -> str:
153155
decimal_signature = int(signature, 16)
154156

155157
return encode_base62(decimal_signature)
158+
159+
160+
def create_storage_content_signature(
161+
resource_id: str, url_signing_secret_key: str, expires_in_millis: int | None = None, version: int = 0
162+
) -> str:
163+
"""Create a secure signature for a resource like a dataset or key-value store.
164+
165+
This signature is used to generate a signed URL for authenticated access, which can be expiring or permanent.
166+
The signature is created using HMAC with the provided secret key and includes
167+
the resource ID, expiration time, and version.
168+
169+
Note: expires_in_millis is optional. If not provided, the signature will not expire.
170+
171+
"""
172+
expires_at = int(time.time() * 1000) + expires_in_millis if expires_in_millis else 0
173+
174+
message_to_sign = f'{version}.{expires_at}.{resource_id}'
175+
hmac = create_hmac_signature(url_signing_secret_key, message_to_sign)
176+
177+
base64url_encoded_payload = base64.urlsafe_b64encode(f'{version}.{expires_at}.{hmac}'.encode())
178+
return base64url_encoded_payload.decode('utf-8')

tests/unit/test_utils.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from __future__ import annotations
22

3+
import base64
34
import io
45
from datetime import datetime, timezone
56
from enum import Enum
67

78
from apify_shared.utils import (
89
create_hmac_signature,
10+
create_storage_content_signature,
911
encode_base62,
1012
filter_out_none_values_recursively,
1113
filter_out_none_values_recursively_internal,
@@ -171,3 +173,38 @@ def test_create_same_hmac() -> None:
171173
message = 'hmac-same-message-to-be-authenticated'
172174
assert create_hmac_signature(secret_key, message) == 'FYMcmTIm3idXqleF1Sw5'
173175
assert create_hmac_signature(secret_key, message) == 'FYMcmTIm3idXqleF1Sw5'
176+
177+
178+
# This test ensures compatibility with the JavaScript version of the same method.
179+
# https://github.com/apify/apify-shared-js/blob/master/packages/utilities/src/storages.ts
180+
def test_create_storage_content_signature() -> None:
181+
# This test uses the same parameters as in JS tests.
182+
secret_key = 'hmac-secret-key'
183+
message = 'resource-id'
184+
185+
signature = create_storage_content_signature(
186+
resource_id=message,
187+
url_signing_secret_key=secret_key,
188+
)
189+
190+
version, expires_at, hmac = base64.urlsafe_b64decode(signature).decode('utf-8').split('.')
191+
192+
assert signature == 'MC4wLjNUd2ZFRTY1OXVmU05zbVM0N2xS'
193+
assert version == '0'
194+
assert expires_at == '0'
195+
assert hmac == '3TwfEE659ufSNsmS47lR'
196+
197+
198+
def test_create_storage_content_signature_with_expiration() -> None:
199+
secret_key = 'hmac-secret-key'
200+
message = 'resource-id'
201+
202+
signature = create_storage_content_signature(
203+
resource_id=message,
204+
url_signing_secret_key=secret_key,
205+
expires_in_millis=10000,
206+
)
207+
208+
version, expires_at, hmac = base64.urlsafe_b64decode(signature).decode('utf-8').split('.')
209+
assert version == '0'
210+
assert expires_at != '0'

0 commit comments

Comments
 (0)