Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ on: [push, pull_request]

jobs:
build:
# Run on every branch push, but avoid duplicate runs when a same-repo PR
# exists: same-repo changes run via the push event, while fork PRs (which
# can't trigger a push in this repo) run via the pull_request event.
if: >-
(github.event_name == 'push' && !github.event.pull_request.head.repo.fork) ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)
runs-on: ubuntu-24.04 # noble equivalent

strategy:
Expand Down
4 changes: 4 additions & 0 deletions cloudinary/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,8 @@ def api_sign_request(params_to_sign, api_secret, algorithm=SIGNATURE_SHA1, signa
- Version 2+: Includes parameter encoding to prevent parameter smuggling
:return: Computed signature
"""
if not api_secret:
raise ValueError("Must supply api_secret")
to_sign = api_string_to_sign(params_to_sign, signature_version)
return compute_hex_hash(to_sign + api_secret, algorithm)

Expand Down Expand Up @@ -875,6 +877,8 @@ def cloudinary_url(source, **options):

signature = None
if sign_url and (not auth_token or auth_token.pop('set_url_signature', False)):
if not api_secret:
raise ValueError("Must supply api_secret")
to_sign = "/".join(__compact([transformation, source_to_sign]))
if long_url_signature:
# Long signature forces SHA256
Expand Down
49 changes: 49 additions & 0 deletions test/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
encode_unicode_url,
base64url_encode,
patch_fetch_format,
cloudinary_url,
cloudinary_scaled_url,
chain_transformations,
generate_transformation_string,
Expand Down Expand Up @@ -1591,6 +1592,54 @@ def test_sign_request_with_signature_version(self):
self.assertEqual(signed_params_v1['signature'], expected_sig_v1)
self.assertEqual(signed_params_v2['signature'], expected_sig_v2)

def test_cloudinary_url_sign_without_secret_raises(self):
"""Signing without a secret should raise ValueError"""
cloudinary.config(cloud_name="test123", api_secret=None)
with self.assertRaises(ValueError) as context:
cloudinary_url("sample", sign_url=True)
self.assertEqual(str(context.exception), "Must supply api_secret")

def test_cloudinary_url_unsigned_without_secret_works(self):
"""Unsigned URL should work without a secret"""
cloudinary.config(cloud_name="test123", api_secret=None)
url, _ = cloudinary_url("sample")
self.assertIn("test123", url)
self.assertNotIn("s--", url)

def test_cloudinary_url_sign_with_secret_works(self):
"""Signing with a secret should work and include signature"""
cloudinary.config(cloud_name="test123", api_key="key", api_secret="secret")
url, _ = cloudinary_url("sample", sign_url=True)
self.assertIn("s--", url)
self.assertIn("test123", url)

def test_cloudinary_url_per_call_secret_override(self):
"""Per-call api_secret override should sign successfully"""
cloudinary.config(cloud_name="test123", api_secret=None)
url, _ = cloudinary_url("sample", sign_url=True, api_secret="override_secret")
self.assertIn("s--", url)
self.assertIn("test123", url)

def test_api_sign_request_without_secret_raises(self):
"""api_sign_request with None secret should raise ValueError"""
params = {"a": "b"}
with self.assertRaises(ValueError) as context:
api_sign_request(params, None)
self.assertEqual(str(context.exception), "Must supply api_secret")

def test_api_sign_request_with_empty_string_raises(self):
"""api_sign_request with empty string secret should raise ValueError"""
params = {"a": "b"}
with self.assertRaises(ValueError) as context:
api_sign_request(params, "")
self.assertEqual(str(context.exception), "Must supply api_secret")

def test_api_sign_request_with_secret_works(self):
"""api_sign_request with a real secret should work"""
params = dict(cloud_name=API_SIGN_REQUEST_CLOUD_NAME, timestamp=1568810420, username="user@cloudinary.com")
signature = api_sign_request(params, API_SIGN_REQUEST_TEST_SECRET)
self.assertEqual(signature, "14c00ba6d0dfdedbc86b316847d95b9e6cd46d94")


if __name__ == '__main__':
unittest.main()
Loading